diff --git a/src/bicep/add-ons/AVD/README.md b/src/bicep/add-ons/AVD/README.md deleted file mode 100644 index 8b1378917..000000000 --- a/src/bicep/add-ons/AVD/README.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/bicep/add-ons/azureVirtualDesktop/LICENSE b/src/bicep/add-ons/azureVirtualDesktop/LICENSE new file mode 100644 index 000000000..14d34a0bb --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Jason Masten + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/bicep/add-ons/azureVirtualDesktop/README.md b/src/bicep/add-ons/azureVirtualDesktop/README.md new file mode 100644 index 000000000..de3a661ad --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/README.md @@ -0,0 +1,32 @@ +# Azure Virtual Desktop Solution + +[**Home**](./README.md) | [**Features**](./docs/features.md) | [**Design**](./docs/design.md) | [**Prerequisites**](./docs/prerequisites.md) | [**Troubleshooting**](./docs/troubleshooting.md) + +This Azure Virtual Desktop (AVD) solution will deploy a fully operational [stamp](https://learn.microsoft.com/azure/architecture/patterns/deployment-stamp) in an Azure subscription adhereing to the [Zero Trust principles](https://learn.microsoft.com/security/zero-trust/azure-infrastructure-avd). Many of the [common features](./docs/features.md) used with AVD have been automated in this solution for your convenience. Be sure to complete the necessary [prerequisites](./docs/prerequisites.md) and to review the parameter descriptions to the understand the consequences of your selections. + +## Deployment Options + +> [!WARNING] +> Failure to complete the [prerequisites](./docs/prerequisites.md) will result in an unsuccessful deployment. + +### Azure Portal + +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fjamasten%2FAzureVirtualDesktop%2Fmain%2Fsolution.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2Fjamasten%2FAzureVirtualDesktop%2Fmain%2FuiDefinition.json) +[![Deploy to Azure Gov](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fjamasten%2FAzureVirtualDesktop%2Fmain%2Fsolution.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2Fjamasten%2FAzureVirtualDesktop%2Fmain%2FuiDefinition.json) + +### PowerShell + +````powershell +New-AzDeployment ` + -Location '' ` + -TemplateFile 'https://raw.githubusercontent.com/jamasten/AzureVirtualDesktop/main/solution.json' ` + -Verbose +```` + +### Azure CLI + +````cli +az deployment sub create \ + --location '' \ + --template-uri 'https://raw.githubusercontent.com/jamasten/AzureVirtualDesktop/main/solution.json' +```` diff --git a/src/bicep/add-ons/azureVirtualDesktop/artifacts/Get-Validations.ps1 b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Get-Validations.ps1 new file mode 100644 index 000000000..5a1457b88 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Get-Validations.ps1 @@ -0,0 +1,260 @@ +[Cmdletbinding()] +Param( + [parameter(Mandatory)] + [string] + $ActiveDirectorySolution, + + [parameter(Mandatory)] + [int] + $CpuCountMax, + + [parameter(Mandatory)] + [int] + $CpuCountMin, + + [parameter(Mandatory)] + [string] + $DomainName, + + [parameter(Mandatory)] + [string] + $Environment, + + [parameter(Mandatory)] + [string] + $ImageDefinitionResourceId, + + [parameter(Mandatory)] + [string] + $Location, + + [parameter(Mandatory)] + [int] + $SessionHostCount, + + [parameter(Mandatory)] + [string] + $StorageService, + + [parameter(Mandatory)] + [string] + $SubscriptionId, + + [parameter(Mandatory)] + [string] + $TenantId, + + [parameter(Mandatory)] + [string] + $UserAssignedIdentityClientId, + + [parameter(Mandatory)] + [string] + $VirtualMachineSize, + + [parameter(Mandatory)] + [string] + $VirtualNetworkName, + + [parameter(Mandatory)] + [string] + $VirtualNetworkResourceGroupName, + + [parameter(Mandatory)] + [string] + $WorkspaceNamePrefix, + + [parameter(Mandatory)] + [string] + $WorkspaceResourceGroupName +) + +function Write-Log +{ + param( + [parameter(Mandatory)] + [string]$Message, + + [parameter(Mandatory)] + [string]$Type + ) + $Path = 'C:\cse.txt' + if(!(Test-Path -Path $Path)) + { + New-Item -Path 'C:\' -Name 'cse.txt' | Out-Null + } + $Timestamp = Get-Date -Format 'MM/dd/yyyy HH:mm:ss.ff' + $Entry = '[' + $Timestamp + '] [' + $Type + '] ' + $Message + $Entry | Out-File -FilePath $Path -Append +} + +$ErrorActionPreference = 'Stop' +$WarningPreference = 'SilentlyContinue' + +try +{ + Connect-AzAccount -Environment $Environment -Tenant $TenantId -Subscription $SubscriptionId -Identity -AccountId $UserAssignedIdentityClientId | Out-Null + $Sku = Get-AzComputeResourceSku -Location $Location | Where-Object {$_.ResourceType -eq "virtualMachines" -and $_.Name -eq $VirtualMachineSize} + + ############################################################## + # Accelerated Networking Validation + ############################################################## + $AcceleratedNetworking = ($Sku.capabilities | Where-Object {$_.name -eq "AcceleratedNetworkingEnabled"}).value + Write-Log -Message "Accelerated Networking Validation Succeeded" -Type 'INFO' + + ############################################################## + # Availability Zone Validation + ############################################################## + $AvailabilityZones = $Sku.locationInfo.zones | Sort-Object + Write-Log -Message "Availability Zone Validation Succeeded" -Type 'INFO' + + ############################################################## + # Azure NetApp Files Validation + ############################################################## + if($StorageService -eq 'AzureNetAppFiles') + { + $Vnet = Get-AzVirtualNetwork -Name $VirtualNetworkName -ResourceGroupName $VirtualNetworkResourceGroupName + $DnsServers = $Vnet.DhcpOptions.DnsServers -join ',' + $SubnetId = ($Vnet.Subnets | Where-Object {$_.Delegations[0].ServiceName -eq "Microsoft.NetApp/volumes"}).Id + if($null -eq $SubnetId -or $SubnetId -eq "") + { + Write-Error -Exception "INVALID AZURE NETAPP FILES CONFIGURATION: A dedicated subnet must be delegated to the ANF resource provider." + } + $DeployAnfAd = "true" + $Accounts = Get-AzResource -ResourceType "Microsoft.NetApp/netAppAccounts" | Where-Object {$_.Location -eq $Location} + if($Accounts.Count -gt 0) + { + $AnfAdCounter = 0 + foreach($Account in $Accounts) + { + $Params = @{ + ResourceGroupName = $Account.ResourceGroupName + ResourceProviderName = 'Microsoft.NetApp' + ResourceType = 'netAppAccounts' + Name = $Account.Name + ApiVersion = '2023-07-01' + Method = 'GET' + } + $AD = ((Invoke-AzRestMethod @Params).Content | ConvertFrom-Json).properties.activeDirectories.activeDirectoryId + if($AD) + { + $AnfAdCounter++ + } + } + if($AnfAdCounter -gt 0) + { + $DeployAnfAd = "false" + } + } + Write-Log -Message "Azure NetApp Files Validation Succeeded" -Type 'INFO' + } + + ############################################################## + # Disk SKU Validation + ############################################################## + if(($Sku.capabilities | Where-Object {$_.name -eq "PremiumIO"}).value -eq $false) + { + Write-Error -Exception "INVALID DISK SKU: The selected VM Size does not support the Premium SKU for managed disks." + } + Write-Log -Message "Disk SKU Validation Succeeded" -Type 'INFO' + + ############################################################## + # Hyper-V Generation Validation + ############################################################## + if(($Sku.capabilities | Where-Object {$_.name -eq "HyperVGenerations"}).value -notlike "*2") + { + Write-Error -Exception "INVALID HYPER-V GENERATION: The selected VM size does not support the selected Image Sku." + } + Write-Log -Message "Hyper-V Generation Validation Succeeded" -Type 'INFO' + + ############################################################## + # Kerberos Encryption Validation + ############################################################## + if($ActiveDirectorySolution -eq 'MicrosoftEntraDomainServices') + { + $KerberosRc4Encryption = (Get-AzResource -Name $DomainName -ExpandProperties).Properties.domainSecuritySettings.kerberosRc4Encryption + if($KerberosRc4Encryption -eq "Enabled") + { + Write-Error -Exception "INVALID KERBEROS ENCRYPTION: The Kerberos Encryption on Azure AD DS does not match your Kerberos Encyrption selection." + } + Write-Log -Message "Kerberos Encryption Validation Succeeded" -Type 'INFO' + } + + ############################################################## + # Trusted Launch Validation + ############################################################## + # Validates the VM Size does not have Trusted Launch disabled and has Hyper-V Generation enabled + # https://learn.microsoft.com/azure/virtual-machines/trusted-launch-faq?tabs=PowerShell#how-can-i-find-vm-sizes-that-support-trusted-launch + $TrustedLaunchDisabled = $Sku.Capabilities | Where-Object {$_.Name -eq "TrustedLaunchDisabled"} | Select-Object -ExpandProperty Value + $HyperVGeneration = $Sku.Capabilities | Where-Object {$_.Name -eq "HyperVGenerations"} | Select-Object -ExpandProperty Value + + if($TrustedLaunchDisabled -or $HyperVGeneration -eq "V1") + { + Write-Error -Exception "INVALID TRUSTED LAUNCH: The selected VM Size does not support Trusted Launch." + } + + # Validates the custom image if applicable + # https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-faq?tabs=PowerShell#how-can-i-validate-if-os-image-supports-trusted-launch + if($ImageDefinitionResourceId -ne 'NotApplicable') + { + $ImageDefinition = Get-AzGalleryImageDefinition -ResourceId $ImageDefinitionResourceId + $SecurityType = ($ImageDefinition.Features | Where-Object {$_.Name -eq 'SecurityType'}).Value + $HyperVGeneration = $ImageDefinition.HyperVGeneration + if($SecurityType -notlike "*TrustedLaunch*" -or $HyperVGeneration -notlike "*V2*") + { + Write-Error -Exception "INVALID TRUSTED LAUNCH: The selected Image Definition does not support Trusted Launch." + } + } + Write-Log -Message "Trusted Launch Validation Succeeded" -Type 'INFO' + + + ############################################################## + # vCPU Count Validation + ############################################################## + # Recommended minimum vCPU is 4 for multisession hosts and 2 for single session hosts. + # Recommended maximum vCPU is 32 for multisession hosts and 128 for single session hosts. + # https://learn.microsoft.com/windows-server/remote/remote-desktop-services/virtual-machine-recs + $vCPUs = [int]($Sku.capabilities | Where-Object {$_.name -eq "vCPUs"}).value + if($vCPUs -lt $CpuCountMin -or $vCPUs -gt $CpuCountMax) + { + Write-Error -Exception "INVALID VCPU COUNT: The selected VM Size does not contain the appropriate amount of vCPUs for Azure Virtual Desktop. https://learn.microsoft.com/windows-server/remote/remote-desktop-services/virtual-machine-recs" + } + Write-Log -Message "vCPU Count Validation Succeeded" -Type 'INFO' + + ############################################################## + # vCPU Quota Validation + ############################################################## + $RequestedCores = $vCPUs * $SessionHostCount + $Family = (Get-AzComputeResourceSku -Location $Location | Where-Object {$_.Name -eq $VirtualMachineSize}).Family + $CpuData = Get-AzVMUsage -Location $Location | Where-Object {$_.Name.Value -eq $Family} + $AvailableCores = $CpuData.Limit - $CpuData.CurrentValue; $RequestedCores = $vCPUs * $SessionHostCount + if($RequestedCores -gt $AvailableCores) + { + Write-Error -Exception "INSUFFICIENT CORE QUOTA: The selected VM size, $VirtualMachineSize, does not have adequate core quota in the selected location." + } + Write-Log -Message "vCPU Quota Validation Succeeded" -Type 'INFO' + + ############################################################## + # AVD Workspace Validation + ############################################################## + $Workspace = Get-AzResource -ResourceGroupName $WorkspaceResourceGroupName -ResourceName $($WorkspaceNamePrefix + '-feed') + Write-Log -Message "Existing Workspace Validation Succeeded" -Type 'INFO' + + Disconnect-AzAccount | Out-Null + + $Output = [pscustomobject][ordered]@{ + acceleratedNetworking = $AcceleratedNetworking.ToLower() + anfDnsServers = if($StorageService -eq "AzureNetAppFiles"){$DnsServers}else{"NotApplicable"} + anfSubnetId = if($StorageService -eq "AzureNetAppFiles"){$SubnetId}else{"NotApplicable"} + anfActiveDirectory = if($StorageService -eq "AzureNetAppFiles"){$DeployAnfAd}else{"false"} + availabilityZones = $AvailabilityZones + existingWorkspace = if($Workspace){"true"}else{"false"} + } + $JsonOutput = $Output | ConvertTo-Json + return $JsonOutput +} +catch +{ + Write-Log -Message $_ -Type 'ERROR' + throw +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/artifacts/Install-AzurePowerShellAzModule.ps1 b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Install-AzurePowerShellAzModule.ps1 new file mode 100644 index 000000000..99cc16517 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Install-AzurePowerShellAzModule.ps1 @@ -0,0 +1,43 @@ +[Cmdletbinding()] +Param( + [parameter(Mandatory)] + [string] + $Installer +) + +function Write-Log +{ + param( + [parameter(Mandatory)] + [string]$Message, + + [parameter(Mandatory)] + [string]$Type + ) + $Path = 'C:\cse.txt' + if(!(Test-Path -Path $Path)) + { + New-Item -Path 'C:\' -Name 'cse.txt' | Out-Null + } + $Timestamp = Get-Date -Format 'MM/dd/yyyy HH:mm:ss.ff' + $Entry = '[' + $Timestamp + '] [' + $Type + '] ' + $Message + $Entry | Out-File -FilePath $Path -Append +} + +$ErrorActionPreference = 'Stop' +$WarningPreference = 'SilentlyContinue' + +try +{ + Start-Process -FilePath 'msiexec.exe' -ArgumentList "/i $Installer /quiet /qn /norestart /passive" -Wait -Passthru | Out-Null + Write-Log -Message 'Installed Azure PowerShell AZ Module' -Type 'INFO' + $Output = [pscustomobject][ordered]@{ + installer = $Installer + } + $Output | ConvertTo-Json +} +catch +{ + Write-Log -Message $_ -Type 'ERROR' + throw +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-AutomationRunbook.ps1 b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-AutomationRunbook.ps1 new file mode 100644 index 000000000..96dd55dc3 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-AutomationRunbook.ps1 @@ -0,0 +1,69 @@ +[Cmdletbinding()] +Param ( + + [parameter(Mandatory)] + [string] + $AutomationAccountName, + + [parameter(Mandatory)] + [string] + $Environment, + + [parameter(Mandatory)] + [string] + $ResourceGroupName, + + [parameter(Mandatory)] + [string] + $RunbookFileName, + + [parameter(Mandatory)] + [string] + $SubscriptionId, + + [parameter(Mandatory)] + [string] + $TenantId, + + [parameter(Mandatory)] + [string] + $UserAssignedIdentityClientId + +) + +$ErrorActionPreference = 'Stop' +$WarningPreference = 'SilentlyContinue' + +try +{ + Connect-AzAccount ` + -Environment $Environment ` + -Tenant $TenantId ` + -Subscription $SubscriptionId ` + -Identity ` + -AccountId $UserAssignedIdentityClientId | Out-Null + + + Import-AzAutomationRunbook ` + -Name $RunbookFileName.Replace('.ps1','') ` + -Path $RunbookFileName ` + -Type 'PowerShell' ` + -AutomationAccountName $AutomationAccountName ` + -ResourceGroupName $ResourceGroupName ` + -Published ` + -Force | Out-Null + + $Output = [pscustomobject][ordered]@{ + runbook = $RunBookName + } + + Disconnect-AzAccount | Out-Null + + $JsonOutput = $Output | ConvertTo-Json + return $JsonOutput +} +catch +{ + Write-Host $_ | Select-Object * + throw +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-AvdDrainMode.ps1 b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-AvdDrainMode.ps1 new file mode 100644 index 000000000..d8c1016c9 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-AvdDrainMode.ps1 @@ -0,0 +1,87 @@ +[Cmdletbinding()] +Param( + [parameter(Mandatory)] + [string] + $Environment, + + [parameter(Mandatory)] + [string] + $HostPoolName, + + [parameter(Mandatory)] + [string] + $HostPoolResourceGroupName, + + [parameter(Mandatory)] + [int] + $SessionHostCount, + + [parameter(Mandatory)] + [int] + $SessionHostIndex, + + [parameter(Mandatory)] + [string] + $SubscriptionId, + + [parameter(Mandatory)] + [string] + $TenantId, + + [parameter(Mandatory)] + [string] + $UserAssignedIdentityClientId, + + [parameter(Mandatory)] + [string] + $VirtualMachineNamePrefix +) + +function Write-Log +{ + param( + [parameter(Mandatory)] + [string]$Message, + + [parameter(Mandatory)] + [string]$Type + ) + $Path = 'C:\cse.txt' + if(!(Test-Path -Path $Path)) + { + New-Item -Path 'C:\' -Name 'cse.txt' | Out-Null + } + $Timestamp = Get-Date -Format 'MM/dd/yyyy HH:mm:ss.ff' + $Entry = '[' + $Timestamp + '] [' + $Type + '] ' + $Message + $Entry | Out-File -FilePath $Path -Append +} + +$ErrorActionPreference = 'Stop' +$WarningPreference = 'SilentlyContinue' + +try +{ + Connect-AzAccount -Environment $Environment -Tenant $TenantId -Subscription $SubscriptionId -Identity -AccountId $UserAssignedIdentityClientId | Out-Null + $SessionHosts = (Get-AzWvdSessionHost -ResourceGroupName $HostPoolResourceGroupName -HostPoolName $HostPoolName).Name + for($i = $SessionHostIndex; $i -lt $($SessionHostIndex + $SessionHostCount); $i++) + { + $VmNameFull = $VirtualMachineNamePrefix + $i.ToString().PadLeft(4,'0') + $SessionHostName = ($SessionHosts | Where-Object {$_ -like "*$VmNameFull*"}).Replace("$HostPoolName/", '') + Update-AzWvdSessionHost -ResourceGroupName $HostPoolResourceGroupName -HostPoolName $HostPoolName -Name $SessionHostName -AllowNewSession:$False | Out-Null + Write-Log -Message "Drain Mode set successfully for session host, $SessionHostName" -Type 'INFO' + } + Write-Log -Message 'Drain Mode Succeeded' -Type 'INFO' + $Output = [pscustomobject][ordered]@{ + hostPool = $HostPoolName + } + + Disconnect-AzAccount | Out-Null + + $JsonOutput = $Output | ConvertTo-Json + return $JsonOutput +} +catch +{ + Write-Log -Message $_ -Type 'ERROR' + throw +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-FileShareScaling.ps1 b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-FileShareScaling.ps1 new file mode 100644 index 000000000..a91e6923e --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-FileShareScaling.ps1 @@ -0,0 +1,73 @@ +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory)] + [string]$Environment, + + [Parameter(Mandatory)] + [string]$FileShareName, + + [Parameter(Mandatory)] + [string]$ResourceGroupName, + + [Parameter(Mandatory)] + [string]$StorageAccountName, + + [Parameter(Mandatory)] + [string]$SubscriptionId +) + +$ErrorActionPreference = 'Stop' + +# Connect to Azure and Import Az Module +Import-Module -Name 'Az.Accounts' +Import-Module -Name 'Az.Storage' +Connect-AzAccount -Environment $Environment -Subscription $SubscriptionId -Identity | Out-Null + +# Get file share +$PFS = Get-AzRmStorageShare -ResourceGroupName $ResourceGroupName -StorageAccountName $StorageAccountName -Name $FileShareName -GetShareUsage + +# Get provisioned capacity and used capacity +$ProvisionedCapacity = $PFS.QuotaGiB +$UsedCapacity = $PFS.ShareUsageBytes +Write-Output "[$StorageAccountName] [$FileShareName] Share Capacity: $($ProvisionedCapacity)GB" +Write-Output "[$StorageAccountName] [$FileShareName] Share Usage: $([math]::Round($UsedCapacity/1GB, 0))GB" + +# Get storage account +$StorageAccount = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName -AccountName $StorageAccountName + +# GB Based Scaling +# No scaling if no usage +if($UsedCapacity -eq 0) +{ + Write-Output "[$StorageAccountName] [$FileShareName] Share Usage is 0GB. No Changes." +} +# Slow scaling up to 500GB +# Increases share quota by 100GB if less than 50GB remains on the share +# This allows time for an AVD Stamp to be rolled out +elseif ($ProvisionedCapacity -lt 500) +{ + if (($ProvisionedCapacity - ($UsedCapacity / ([Math]::Pow(2,30)))) -lt 50) { + Write-Output "[$StorageAccountName] [$FileShareName] Share Usage has surpassed the Share Quota remaining threshold of 50GB. Increasing the file share quota by 100GB." + $Quota = $ProvisionedCapacity + 100 + Update-AzRmStorageShare -StorageAccount $StorageAccount -Name $FileShareName -QuotaGiB $Quota | Out-Null + Write-Output "[$StorageAccountName] [$FileShareName] New Capacity: $($Quota)GB" + } + else { + Write-Output "[$StorageAccountName] [$FileShareName] Share Usage is below Share Quota remaining threshold of 50GB. No Changes." + } +} +# Aggressive scaling +# Increases share quota by 500GB if less than 500GB remains on the share +# This ensures plenty of space is available during mass onboarding +else +{ + if (($ProvisionedCapacity - ($UsedCapacity / ([Math]::Pow(2,30)))) -lt 500) { + Write-Output "[$StorageAccountName] [$FileShareName] Share Usage has surpassed the Share Quota remaining threshold of 500GB. Increasing the file share quota by 500GB." + $Quota = $ProvisionedCapacity + 500 + Update-AzRmStorageShare -StorageAccount $StorageAccount -Name $FileShareName -QuotaGiB $Quota | Out-Null + Write-Output "[$StorageAccountName] [$FileShareName] New Capacity: $($Quota)GB" + } + else { + Write-Output "[$StorageAccountName] [$FileShareName] Share Usage is below Share Quota remaining threshold of 500GB. No Changes." + } +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-HostPoolScaling.ps1 b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-HostPoolScaling.ps1 new file mode 100644 index 000000000..90298f1cc --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-HostPoolScaling.ps1 @@ -0,0 +1,846 @@ +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory)] + [string]$BeginPeakTime, + + [Parameter(Mandatory)] + [string]$EndPeakTime, + + [Parameter(Mandatory)] + [string]$EnvironmentName, + + [Parameter(Mandatory)] + [string]$HostPoolName, + + [Parameter(Mandatory)] + [int]$LimitSecondsToForceLogOffUser, + + [Parameter(Mandatory)] + [string]$LogOffMessageBody, + + [Parameter(Mandatory)] + [string]$LogOffMessageTitle, + + [Parameter(Mandatory)] + [string]$MaintenanceTagName, + + [Parameter(Mandatory)] + [int]$MinimumNumberOfRDSH, + + [Parameter(Mandatory)] + [string]$ResourceGroupName, + + [Parameter(Mandatory)] + [double]$SessionThresholdPerCPU, + + [Parameter(Mandatory)] + [string]$SubscriptionId, + + [Parameter(Mandatory)] + [string]$TenantId, + + [Parameter(Mandatory)] + [string]$TimeDifference +) + +try +{ + [int]$StatusCheckTimeOut = (60 * 60) # 1 hr + [string[]]$DesiredRunningStates = @('Available', 'NeedsAssistance') + [string[]]$TimeDiffHrsMin = "$($TimeDifference):0".Split(':') + + + #region helper/common functions, set exec policies, set TLS 1.2 security protocol, log rqt params + # Function to return local time converted from UTC + function Get-LocalDateTime + { + return (Get-Date).ToUniversalTime().AddHours($TimeDiffHrsMin[0]).AddMinutes($TimeDiffHrsMin[1]) + } + + function Write-Log + { + # Note: this is required to support param such as ErrorAction + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Message, + + [switch]$Err, + + [switch]$Warn, + + [Parameter(Mandatory = $true)] + [string]$HostPoolName + ) + + [string]$MessageTimeStamp = (Get-LocalDateTime).ToString('yyyy-MM-dd HH:mm:ss') + $Message = "[$($MyInvocation.ScriptLineNumber)] [$($HostPoolName)] $Message" + [string]$WriteMessage = "[$($MessageTimeStamp)] $Message" + + if ($Err) + { + Write-Error $WriteMessage + $Message = "ERROR: $Message" + } + elseif ($Warn) + { + Write-Warning $WriteMessage + $Message = "WARN: $Message" + } + else + { + Write-Output $WriteMessage + } + } + + function Set-nVMsToStartOrStop + { + param ( + [Parameter(Mandatory = $true)] + [int]$nRunningVMs, + + [Parameter(Mandatory = $true)] + [int]$nRunningCores, + + [Parameter(Mandatory = $true)] + [int]$nUserSessions, + + [Parameter(Mandatory = $true)] + [int]$MaxUserSessionsPerVM, + + [switch]$InPeakHours, + + [Parameter(Mandatory = $true)] + [hashtable]$Res + ) + + # check if need to adjust min num of running session hosts required if the number of user sessions is close to the max allowed by the min num of running session hosts required + [double]$MaxUserSessionsThreshold = 0.9 + [int]$MaxUserSessionsThresholdCapacity = [math]::Floor($MinimumNumberOfRDSH * $MaxUserSessionsPerVM * $MaxUserSessionsThreshold) + if ($nUserSessions -gt $MaxUserSessionsThresholdCapacity) + { + $MinimumNumberOfRDSH = [math]::Ceiling($nUserSessions / ($MaxUserSessionsPerVM * $MaxUserSessionsThreshold)) + Write-Log -HostPoolName $HostPoolName -Message "Number of user sessions is more than $($MaxUserSessionsThreshold * 100) % of the max number of sessions allowed with minimum number of running session hosts required ($MaxUserSessionsThresholdCapacity). Adjusted minimum number of running session hosts required to $MinimumNumberOfRDSH" + } + + # Check if minimum number of session hosts are running + if ($nRunningVMs -lt $MinimumNumberOfRDSH) + { + $res.nVMsToStart = $MinimumNumberOfRDSH - $nRunningVMs + Write-Log -HostPoolName $HostPoolName -Message "Number of running session host is less than minimum required. Need to start $($res.nVMsToStart) VMs" + } + + if ($InPeakHours) + { + [double]$nUserSessionsPerCore = $nUserSessions / $nRunningCores + # In peak hours: check if current capacity is meeting the user demands + if ($nUserSessionsPerCore -gt $SessionThresholdPerCPU) + { + $res.nCoresToStart = [math]::Ceiling(($nUserSessions / $SessionThresholdPerCPU) - $nRunningCores) + Write-Log -HostPoolName $HostPoolName -Message "[In peak hours] Number of user sessions per Core is more than the threshold. Need to start $($res.nCoresToStart) cores" + } + + return + } + + if ($nRunningVMs -gt $MinimumNumberOfRDSH) + { + # Calculate the number of session hosts to stop + $res.nVMsToStop = $nRunningVMs - $MinimumNumberOfRDSH + Write-Log -HostPoolName $HostPoolName -Message "[Off peak hours] Number of running session host is greater than minimum required. Need to stop $($res.nVMsToStop) VMs" + } + } + + # Function to wait for background jobs + function Wait-ForJobs + { + param ([array]$Jobs = @()) + + Write-Log -HostPoolName $HostPoolName -Message "Wait for $($Jobs.Count) jobs" + $StartTime = Get-Date + [string]$StatusInfo = '' + while ($true) + { + if ((Get-Date).Subtract($StartTime).TotalSeconds -ge $StatusCheckTimeOut) + { + throw "Jobs status check timed out. Taking more than $StatusCheckTimeOut seconds. $StatusInfo" + } + $StatusInfo = "[Check jobs status] Total: $($Jobs.Count), $(($Jobs | Group-Object State | ForEach-Object { "$($_.Name): $($_.Count)" }) -join ', ')" + Write-Log -HostPoolName $HostPoolName -Message $StatusInfo + if (!($Jobs | Where-Object { $_.State -ieq 'Running' })) + { + break + } + Start-Sleep -Seconds 30 + } + + [array]$IncompleteJobs = @($Jobs | Where-Object { $_.State -ine 'Completed' }) + if ($IncompleteJobs) + { + throw "$($IncompleteJobs.Count)/$($Jobs.Count) jobs did not complete successfully: $($IncompleteJobs | Format-List -Force | Out-String)" + } + } + + function Get-SessionHostName + { + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + $SessionHost + ) + + return $SessionHost.Name.Split('/')[-1] + } + + function TryUpdateSessionHostDrainMode + { + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable]$VM, + + [switch]$AllowNewSession + ) + Begin { } + Process + { + $SessionHost = $VM.SessionHost + if ($SessionHost.AllowNewSession -eq $AllowNewSession) + { + return + } + + [string]$SessionHostName = $VM.SessionHostName + Write-Log -HostPoolName $HostPoolName -Message "Update session host '$SessionHostName' to set allow new sessions to $AllowNewSession" + if ($PSCmdlet.ShouldProcess($SessionHostName, "Update session host to set allow new sessions to $AllowNewSession")) + { + try + { + $SessionHost = $VM.SessionHost = Update-AzWvdSessionHost -ResourceGroupName $ResourceGroupName -Name $SessionHostName -AllowNewSession:$AllowNewSession + + if ($SessionHost.AllowNewSession -ne $AllowNewSession) + { + throw $SessionHost + } + } + catch + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Failed to update the session host '$SessionHostName' to set allow new sessions to $($AllowNewSession): $($PSItem | Format-List -Force | Out-String)" + } + } + } + End { } + } + + function TryForceLogOffUser + { + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + $Session + ) + Begin { } + Process + { + [string[]]$Toks = $Session.Name.Split('/') + [string]$SessionHostName = $Toks[1] + [string]$SessionID = $Toks[-1] + [string]$User = $Session.ActiveDirectoryUserName + + try + { + Write-Log -HostPoolName $HostPoolName -Message "Force log off user: '$User', session ID: $SessionID" + if ($PSCmdlet.ShouldProcess($SessionID, 'Force log off user with session ID')) + { + # Note: -SessionHostName param is case sensitive, so the command will fail if it's case is modified + Remove-AzWvdUserSession -ResourceGroupName $ResourceGroupName -SessionHostName $SessionHostName -Id $SessionID -Force + } + } + catch + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Failed to force log off user: '$User', session ID: $SessionID $($PSItem | Format-List -Force | Out-String)" + } + } + End { } + } + + function TryResetSessionHostDrainModeAndUserSessions + { + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable]$VM + ) + Begin { } + Process + { + TryUpdateSessionHostDrainMode -VM $VM -AllowNewSession:$true + + $SessionHost = $VM.SessionHost + [string]$SessionHostName = $VM.SessionHostName + if (!$SessionHost.Session) + { + return + } + + Write-Log -HostPoolName $HostPoolName -Warn -Message "Session host '$SessionHostName' still has $($SessionHost.Session) sessions left behind in broker DB" + + [array]$UserSessions = @() + Write-Log -HostPoolName $HostPoolName -Message "Get all user sessions from session host '$SessionHostName'" + try + { + $UserSessions = @(Get-AzWvdUserSession -ResourceGroupName $ResourceGroupName -SessionHostName $SessionHostName) + } + catch + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Failed to retrieve user sessions of session host '$SessionHostName': $($PSItem | Format-List -Force | Out-String)" + return + } + + Write-Log -HostPoolName $HostPoolName -Message "Force log off $($UserSessions.Count) users on session host: '$SessionHostName'" + $UserSessions | TryForceLogOffUser + } + End { } + } + + Set-ExecutionPolicy -ExecutionPolicy Undefined -Scope Process -Force -Confirm:$false + if (!$SkipAuth) + { + # Note: this requires admin priviledges + Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope LocalMachine -Force -Confirm:$false + } + + # Note: https://stackoverflow.com/questions/41674518/powershell-setting-security-protocol-to-tls-1-2 + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + Write-Log -HostPoolName $HostPoolName -Message "Request params: $($RqtParams | Format-List -Force | Out-String)" + #endregion + + + #region azure auth, ctx + # Azure auth + $AzContext = $null + try + { + $AzAuth = Connect-AzAccount -Environment $EnvironmentName -Tenant $TenantId -Subscription $SubscriptionId -Identity + if (!$AzAuth -or !$AzAuth.Context) { + throw $AzAuth + } + $AzContext = $AzAuth.Context + } + catch + { + throw [System.Exception]::new('Failed to authenticate Azure with application ID, tenant ID, subscription ID', $PSItem.Exception) + } + Write-Log -HostPoolName $HostPoolName -Message "Successfully authenticated with Azure using service principal: $($AzContext | Format-List -Force | Out-String)" + #endregion + + + #region validate host pool, validate / update HostPool load balancer type, ensure there is at least 1 session host, get num of user sessions + # Validate and get HostPool info + $HostPool = $null + try + { + Write-Log -HostPoolName $HostPoolName -Message "Get Hostpool info of '$HostPoolName' in resource group '$ResourceGroupName'" + $HostPool = Get-AzWvdHostPool -ResourceGroupName $ResourceGroupName -Name $HostPoolName + + if (!$HostPool) + { + throw $HostPool + } + } + catch + { + throw [System.Exception]::new("Failed to get Hostpool info of '$HostPoolName' in resource group '$ResourceGroupName'. Ensure that you have entered the correct values", $PSItem.Exception) + } + + # Ensure HostPool load balancer type is not persistent + if ($HostPool.LoadBalancerType -ieq 'Persistent') + { + throw "HostPool '$HostPoolName' is configured with 'Persistent' load balancer type. Scaling tool only supports these load balancer types: BreadthFirst, DepthFirst" + } + + Write-Log -HostPoolName $HostPoolName -Message 'Get all session hosts' + + $SessionHosts = @(Get-AzWvdSessionHost -ResourceGroupName $ResourceGroupName -HostPoolName $HostPoolName) + + if (!$SessionHosts) + { + Write-Log -HostPoolName $HostPoolName -Message "There are no session hosts in the Hostpool '$HostPoolName'. Ensure that hostpool has session hosts" + Write-Log -HostPoolName $HostPoolName -Message 'End' + return + } + + Write-Log -HostPoolName $HostPoolName -Message 'Get number of user sessions in Hostpool' + + [int]$nUserSessions = @(Get-AzWvdUserSession -ResourceGroupName $ResourceGroupName -HostPoolName $HostPoolName).Count + + # Set up breadth 1st load balacing type + # Note: breadth 1st is enforced on AND off peak hours to simplify the things with scaling in the start/end of peak hours + if (!$SkipUpdateLoadBalancerType -and $HostPool.LoadBalancerType -ine 'BreadthFirst') + { + Write-Log -HostPoolName $HostPoolName -Message "Update HostPool with 'BreadthFirst' load balancer type (current: '$($HostPool.LoadBalancerType)')" + if ($PSCmdlet.ShouldProcess($HostPoolName, "Update HostPool with BreadthFirstLoadBalancer type (current: '$($HostPool.LoadBalancerType)')")) + { + $HostPool = Update-AzWvdHostPool -ResourceGroupName $ResourceGroupName -Name $HostPoolName -LoadBalancerType 'BreadthFirst' + } + } + + Write-Log -HostPoolName $HostPoolName -Message "HostPool info: $($HostPool | Format-List -Force | Out-String)" + Write-Log -HostPoolName $HostPoolName -Message "Number of session hosts in the HostPool: $($SessionHosts.Count)" + #endregion + + + #region determine if on/off peak hours + # Convert local time, begin peak time & end peak time from UTC to local time + $CurrentDateTime = Get-LocalDateTime + $BeginPeakDateTime = [datetime]::Parse($CurrentDateTime.ToShortDateString() + ' ' + $BeginPeakTime) + $EndPeakDateTime = [datetime]::Parse($CurrentDateTime.ToShortDateString() + ' ' + $EndPeakTime) + + # Adjust peak times to make sure begin peak time is always before end peak time + if ($EndPeakDateTime -lt $BeginPeakDateTime) + { + if ($CurrentDateTime -lt $EndPeakDateTime) + { + $BeginPeakDateTime = $BeginPeakDateTime.AddDays(-1) + } + else + { + $EndPeakDateTime = $EndPeakDateTime.AddDays(1) + } + } + + Write-Log -HostPoolName $HostPoolName -Message "Using current time: $($CurrentDateTime.ToString('yyyy-MM-dd HH:mm:ss')), begin peak time: $($BeginPeakDateTime.ToString('yyyy-MM-dd HH:mm:ss')), end peak time: $($EndPeakDateTime.ToString('yyyy-MM-dd HH:mm:ss'))" + + [bool]$InPeakHours = ($BeginPeakDateTime -le $CurrentDateTime -and $CurrentDateTime -le $EndPeakDateTime) + if ($InPeakHours) + { + Write-Log -HostPoolName $HostPoolName -Message 'In peak hours' + } + else + { + Write-Log -HostPoolName $HostPoolName -Message 'Off peak hours' + } + #endregion + + + #region get all session hosts, VMs & user sessions info and compute workload + # Note: session host is considered "running" if its running AND is in desired states AND allowing new sessions + # Number of session hosts that are running, are in desired states and allowing new sessions + [int]$nRunningVMs = 0 + # Number of cores that are running, are in desired states and allowing new sessions + [int]$nRunningCores = 0 + # Object that contains all session host objects, VM instance objects except the ones that are under maintenance + $VMs = @{ } + # Object that contains the number of cores for each VM size SKU + $VMSizeCores = @{ } + # Number of user sessions reported by each session host that is running, is in desired state and allowing new sessions + [int]$nUserSessionsFromAllRunningVMs = 0 + + # Populate all session hosts objects + foreach ($SessionHost in $SessionHosts) + { + [string]$SessionHostName = Get-SessionHostName -SessionHost $SessionHost + $VMs.Add($SessionHostName.Split('.')[0].ToLower(), @{ 'SessionHostName' = $SessionHostName; 'SessionHost' = $SessionHost; 'Instance' = $null }) + } + + Write-Log -HostPoolName $HostPoolName -Message 'Get all VMs, check session host status and get usage info' + foreach ($VMInstance in (Get-AzVM -Status)) + { + if (!$VMs.ContainsKey($VMInstance.Name.ToLower())) + { + # This VM is not a WVD session host + continue + } + [string]$VMName = $VMInstance.Name.ToLower() + if ($VMInstance.Tags.Keys -contains $MaintenanceTagName) + { + Write-Log -HostPoolName $HostPoolName -Message "VM '$VMName' is in maintenance and will be ignored" + $VMs.Remove($VMName) + continue + } + + $VM = $VMs[$VMName] + $SessionHost = $VM.SessionHost + if (($SessionHost.VirtualMachineId) -and $VMInstance.VmId -ine $SessionHost.VirtualMachineId) + { + # This VM is not a WVD session host + continue + } + + if ($VM.Instance) + { + throw "More than 1 VM found in Azure with same session host name '$($VM.SessionHostName)' (This is not supported): $($VMInstance | Format-List -Force | Out-String)$($VM.Instance | Format-List -Force | Out-String)" + } + + $VM.Instance = $VMInstance + + Write-Log -HostPoolName $HostPoolName -Message "Session host: '$($VM.SessionHostName)', power state: '$($VMInstance.PowerState)', status: '$($SessionHost.Status)', update state: '$($SessionHost.UpdateState)', sessions: $($SessionHost.Session), allow new session: $($SessionHost.AllowNewSession)" + # Check if we know how many cores are in this VM + if (!$VMSizeCores.ContainsKey($VMInstance.HardwareProfile.VmSize)) + { + Write-Log -HostPoolName $HostPoolName -Message "Get all VM sizes in location: $($VMInstance.Location)" + foreach ($VMSize in (Get-AzVMSize -Location $VMInstance.Location)) + { + if (!$VMSizeCores.ContainsKey($VMSize.Name)) + { + $VMSizeCores.Add($VMSize.Name, $VMSize.NumberOfCores) + } + } + } + + if ($VMInstance.PowerState -ieq 'VM running') + { + if ($SessionHost.Status -notin $DesiredRunningStates) + { + Write-Log -HostPoolName $HostPoolName -Warn -Message 'VM is in running state but session host is not and so it will be ignored (this could be because the VM was just started and has not connected to broker yet)' + } + if (!$SessionHost.AllowNewSession) + { + Write-Log -HostPoolName $HostPoolName -Warn -Message 'VM is in running state but session host is not allowing new sessions and so it will be ignored' + } + + if ($SessionHost.Status -in $DesiredRunningStates -and $SessionHost.AllowNewSession) + { + ++$nRunningVMs + $nRunningCores += $VMSizeCores[$VMInstance.HardwareProfile.VmSize] + $nUserSessionsFromAllRunningVMs += $SessionHost.Session + } + } + else + { + if ($SessionHost.Status -in $DesiredRunningStates) + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "VM is not in running state but session host is (this could be because the VM was just stopped and broker doesn't know that yet)" + } + } + } + + if ($nUserSessionsFromAllRunningVMs -ne $nUserSessions) + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Sum of user sessions reported by every running session host ($nUserSessionsFromAllRunningVMs) is not equal to the total number of user sessions reported by the host pool ($nUserSessions)" + } + + $nUserSessions = $nUserSessionsFromAllRunningVMs + # Check if we need to override the number of user sessions for simulation / testing purpose + if ($null -ne $OverrideNUserSessions) + { + $nUserSessions = $OverrideNUserSessions + } + + # Make sure VM instance was found in Azure for every session host + [int]$nVMsWithoutInstance = @($VMs.Values | Where-Object { !$_.Instance }).Count + if ($nVMsWithoutInstance) + { + throw "There are $nVMsWithoutInstance/$($VMs.Count) session hosts whose VM instance was not found in Azure" + } + + if (!$nRunningCores) + { + $nRunningCores = 1 + } + + Write-Log -HostPoolName $HostPoolName -Message "Number of running session hosts: $nRunningVMs of total $($VMs.Count)" + Write-Log -HostPoolName $HostPoolName -Message "Number of user sessions: $nUserSessions of total allowed $($nRunningVMs * $HostPool.MaxSessionLimit)" + Write-Log -HostPoolName $HostPoolName -Message "Number of user sessions per Core: $($nUserSessions / $nRunningCores), threshold: $SessionThresholdPerCPU" + Write-Log -HostPoolName $HostPoolName -Message "Minimum number of running session hosts required: $MinimumNumberOfRDSH" + + # Check if minimum num of running session hosts required is higher than max allowed + if ($VMs.Count -le $MinimumNumberOfRDSH) + { + Write-Log -HostPoolName $HostPoolName -Warn -Message 'Minimum number of RDSH is set higher than or equal to total number of session hosts' + } + #endregion + + + #region determine number of session hosts to start/stop if any + # Now that we have all the info about the session hosts & their usage, figure how many session hosts to start/stop depending on in/off peak hours and the demand [Ops = operations to perform] + $Ops = @{ + nVMsToStart = 0 + nCoresToStart = 0 + nVMsToStop = 0 + } + + Set-nVMsToStartOrStop -nRunningVMs $nRunningVMs -nRunningCores $nRunningCores -nUserSessions $nUserSessions -MaxUserSessionsPerVM $HostPool.MaxSessionLimit -InPeakHours:$InPeakHours -Res $Ops + #endregion + + + #region start any session hosts if need to + # Check if we have any session hosts to start + if ($Ops.nVMsToStart -or $Ops.nCoresToStart) + { + if ($nRunningVMs -eq $VMs.Count) + { + Write-Log -HostPoolName $HostPoolName -Message 'All session hosts are running' + Write-Log -HostPoolName $HostPoolName -Message 'End' + return + } + + # Object that contains names of session hosts that will be started + # $StartSessionHostFullNames = @{ } + # Array that contains jobs of starting the session hosts + [array]$StartVMjobs = @() + + Write-Log -HostPoolName $HostPoolName -Message 'Find session hosts that are stopped and allowing new sessions' + foreach ($VM in $VMs.Values) + { + if (!$Ops.nVMsToStart -and !$Ops.nCoresToStart) + { + # Done with starting session hosts that needed to be + break + } + if ($VM.Instance.PowerState -ieq 'VM running') + { + continue + } + if ($VM.SessionHost.UpdateState -ine 'Succeeded') + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Session host '$($VM.SessionHostName)' may not be healthy" + } + + [string]$SessionHostName = $VM.SessionHostName + + if (!$VM.SessionHost.AllowNewSession) + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Session host '$SessionHostName' is not allowing new sessions and so it will not be started" + continue + } + + Write-Log -HostPoolName $HostPoolName -Message "Start session host '$SessionHostName' as a background job" + if ($PSCmdlet.ShouldProcess($SessionHostName, 'Start session host as a background job')) + { + # $StartSessionHostFullNames.Add($VM.SessionHost.Name, $null) + $StartVMjobs += ($VM.Instance | Start-AzVM -AsJob) + } + + --$Ops.nVMsToStart + if ($Ops.nVMsToStart -lt 0) + { + $Ops.nVMsToStart = 0 + } + + $Ops.nCoresToStart -= $VMSizeCores[$VM.Instance.HardwareProfile.VmSize] + if ($Ops.nCoresToStart -lt 0) + { + $Ops.nCoresToStart = 0 + } + } + + # Check if there were enough number of session hosts to start + if ($Ops.nVMsToStart -or $Ops.nCoresToStart) + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Not enough session hosts to start. Still need to start maximum of either $($Ops.nVMsToStart) VMs or $($Ops.nCoresToStart) cores" + } + + # Wait for those jobs to start the session hosts + Wait-ForJobs $StartVMjobs + + Write-Log -HostPoolName $HostPoolName -Message 'All jobs completed' + Write-Log -HostPoolName $HostPoolName -Message 'End' + return + } + #endregion + + + #region stop any session hosts if need to + if (!$Ops.nVMsToStop) + { + Write-Log -HostPoolName $HostPoolName -Message 'No need to start/stop any session hosts' + Write-Log -HostPoolName $HostPoolName -Message 'End' + return + } + + # Object that contains names of session hosts that will be stopped + # $StopSessionHostFullNames = @{ } + # Array that contains jobs of stopping the session hosts + [array]$StopVMjobs = @() + $VMsToStop = @{ } + [array]$VMsToStopAfterLogOffTimeOut = @() + + Write-Log -HostPoolName $HostPoolName -Message 'Find session hosts that are running and allowing new sessions, sort them by number of user sessions' + foreach ($VM in ($VMs.Values | Where-Object { $_.Instance.PowerState -ieq 'VM running' -and $_.SessionHost.AllowNewSession } | Sort-Object { $_.SessionHost.Session })) + { + if (!$Ops.nVMsToStop) + { + # Done with stopping session hosts that needed to be + break + } + $SessionHost = $VM.SessionHost + [string]$SessionHostName = $VM.SessionHostName + + if ($SessionHost.Session -and !$LimitSecondsToForceLogOffUser) + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Session host '$SessionHostName' has $($SessionHost.Session) sessions but limit seconds to force log off user is set to 0, so will not stop any more session hosts (https://aka.ms/wvdscale#how-the-scaling-tool-works)" + # Note: why break ? Because the list this loop iterates through is sorted by number of sessions, if it hits this, the rest of items in the loop will also hit this + break + } + + TryUpdateSessionHostDrainMode -VM $VM -AllowNewSession:$false + $SessionHost = $VM.SessionHost + + # Note: check if there were new user sessions since session host info was 1st fetched + if ($SessionHost.Session -and !$LimitSecondsToForceLogOffUser) + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Session host '$SessionHostName' has $($SessionHost.Session) sessions but limit seconds to force log off user is set to 0, so will not stop any more session hosts (https://aka.ms/wvdscale#how-the-scaling-tool-works)" + TryUpdateSessionHostDrainMode -VM $VM -AllowNewSession:$true + $SessionHost = $VM.SessionHost + continue + } + + if ($SessionHost.Session) + { + [array]$VM.UserSessions = @() + Write-Log -HostPoolName $HostPoolName -Message "Get all user sessions from session host '$SessionHostName'" + try + { + # Note: Get-AzWvdUserSession roundtrips the input param SessionHostName and its case, so if lower case is specified, command will return lower case as well + $VM.UserSessions = @(Get-AzWvdUserSession -ResourceGroupName $ResourceGroupName -HostPoolName $HostPoolName -SessionHostName $SessionHostName) + } + catch + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Failed to retrieve user sessions of session host '$SessionHostName': $($PSItem | Format-List -Force | Out-String)" + } + + Write-Log -HostPoolName $HostPoolName -Message "Send log off message to active user sessions on session host: '$SessionHostName'" + foreach ($Session in $VM.UserSessions) + { + if($Session.SessionState -ine 'Active') + { + continue + } + + [string]$SessionID = $Session.Name.Split('/')[-1] + [string]$User = $Session.ActiveDirectoryUserName + + try + { + Write-Log -HostPoolName $HostPoolName -Message "Send a log off message to user: '$User', session ID: $SessionID" + if ($PSCmdlet.ShouldProcess($SessionID, 'Send a log off message to user with session ID')) + { + # Note: -SessionHostName param is case sensitive, so the command will fail if it's case is modified + Send-AzWvdUserSessionMessage -ResourceGroupName $ResourceGroupName -HostPoolName $HostPoolName -SessionHostName $SessionHostName -UserSessionId $SessionID -MessageTitle $LogOffMessageTitle -MessageBody "$LogOffMessageBody You will be logged off in $LimitSecondsToForceLogOffUser seconds" + } + } + catch + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Failed to send a log off message to user: '$User', session ID: $SessionID $($PSItem | Format-List -Force | Out-String)" + } + } + $VMsToStopAfterLogOffTimeOut += $VM + } + else + { + Write-Log -HostPoolName $HostPoolName -Message "Stop session host '$SessionHostName' as a background job" + if ($PSCmdlet.ShouldProcess($SessionHostName, 'Stop session host as a background job')) + { + # $StopSessionHostFullNames.Add($SessionHost.Name, $null) + $StopVMjobs += ($VM.StopJob = $VM.Instance | Stop-AzVM -Force -AsJob) + $VMsToStop.Add($SessionHostName, $VM) + } + } + + --$Ops.nVMsToStop + if ($Ops.nVMsToStop -lt 0) { + $Ops.nVMsToStop = 0 + } + } + + if ($VMsToStopAfterLogOffTimeOut) + { + Write-Log -HostPoolName $HostPoolName -Message "Wait $LimitSecondsToForceLogOffUser seconds for users to log off" + if ($PSCmdlet.ShouldProcess("for $LimitSecondsToForceLogOffUser seconds", 'Wait for users to log off')) + { + Start-Sleep -Seconds $LimitSecondsToForceLogOffUser + } + + Write-Log -HostPoolName $HostPoolName -Message "Force log off users and stop remaining $($VMsToStopAfterLogOffTimeOut.Count) session hosts" + foreach ($VM in $VMsToStopAfterLogOffTimeOut) + { + [string]$SessionHostName = $VM.SessionHostName + + Write-Log -HostPoolName $HostPoolName -Message "Force log off $($VM.UserSessions.Count) users on session host: '$SessionHostName'" + $VM.UserSessions | TryForceLogOffUser + + Write-Log -HostPoolName $HostPoolName -Message "Stop session host '$SessionHostName' as a background job" + if ($PSCmdlet.ShouldProcess($SessionHostName, 'Stop session host as a background job')) + { + # $StopSessionHostFullNames.Add($VM.SessionHost.Name, $null) + $StopVMjobs += ($VM.StopJob = $VM.Instance | Stop-AzVM -Force -AsJob) + $VMsToStop.Add($SessionHostName, $VM) + } + } + } + + # Check if there were enough number of session hosts to stop + if ($Ops.nVMsToStop) + { + Write-Log -HostPoolName $HostPoolName -Warn -Message "Not enough session hosts to stop. Still need to stop $($Ops.nVMsToStop) VMs" + } + + # Wait for those jobs to stop the session hosts + Write-Log -HostPoolName $HostPoolName -Message "Wait for $($StopVMjobs.Count) jobs" + $StartTime = Get-Date + while ($true) + { + if ((Get-Date).Subtract($StartTime).TotalSeconds -ge $StatusCheckTimeOut) + { + break + } + if (!($StopVMjobs | Where-Object { $_.State -ieq 'Running' })) + { + break + } + + Write-Log -HostPoolName $HostPoolName -Message "[Check jobs status] Total: $($StopVMjobs.Count), $(($StopVMjobs | Group-Object State | ForEach-Object { "$($_.Name): $($_.Count)" }) -join ', ')" + + $VMstoResetDrainModeAndSessions = @($VMsToStop.Values | Where-Object { $_.StopJob.State -ine 'Running' }) + foreach ($VM in $VMstoResetDrainModeAndSessions) + { + TryResetSessionHostDrainModeAndUserSessions -VM $VM + $VMsToStop.Remove($VM.SessionHostName) + } + if (!$VMstoResetDrainModeAndSessions) + { + Start-Sleep -Seconds 30 + } + } + + [string]$StopVMJobsStatusInfo = "[Check jobs status] Total: $($StopVMjobs.Count), $(($StopVMjobs | Group-Object State | ForEach-Object { "$($_.Name): $($_.Count)" }) -join ', ')" + Write-Log -HostPoolName $HostPoolName -Message $StopVMJobsStatusInfo + + $VMsToStop.Values | TryResetSessionHostDrainModeAndUserSessions + + if ((Get-Date).Subtract($StartTime).TotalSeconds -ge $StatusCheckTimeOut) + { + throw "Jobs status check timed out. Taking more than $StatusCheckTimeOut seconds. $StopVMJobsStatusInfo" + } + + [array]$IncompleteJobs = @($StopVMjobs | Where-Object { $_.State -ine 'Completed' }) + if ($IncompleteJobs) + { + throw "$($IncompleteJobs.Count)/$($StopVMjobs.Count) jobs did not complete successfully: $($IncompleteJobs | Format-List -Force | Out-String)" + } + + Write-Log -HostPoolName $HostPoolName -Message 'All jobs completed' + Write-Log -HostPoolName $HostPoolName -Message 'End' + return + #endregion +} +catch +{ + $ErrContainer = $PSItem + # $ErrContainer = $_ + + [string]$ErrMsg = $ErrContainer | Format-List -Force | Out-String + $ErrMsg += "Version: $Version`n" + + if (Get-Command 'Write-Log' -ErrorAction:SilentlyContinue) + { + Write-Log -HostPoolName $HostPoolName -Err -Message $ErrMsg -ErrorAction:Continue + } + else + { + Write-Error $ErrMsg -ErrorAction:Continue + } + + throw [System.Exception]::new($ErrMsg, $ErrContainer.Exception) +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-NtfsPermissions.ps1 b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-NtfsPermissions.ps1 new file mode 100644 index 000000000..01c9ba3a5 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-NtfsPermissions.ps1 @@ -0,0 +1,287 @@ +param +( + [Parameter(Mandatory=$false)] + [String]$ActiveDirectorySolution, + + [Parameter(Mandatory=$false)] + [String]$DomainAccountType = "ComputerAccount", + + [Parameter(Mandatory)] + [String]$DomainJoinPassword, + + [Parameter(Mandatory)] + [String]$DomainJoinUserPrincipalName, + + [Parameter(Mandatory=$false)] + [String]$Environment, + + [Parameter(Mandatory)] + [String]$FslogixContainerType, + + [Parameter(Mandatory=$false)] + [String]$Netbios, + + [Parameter(Mandatory=$false)] + [String]$OrganizationalUnitPath, + + [Parameter(Mandatory=$false)] + [String]$ResourceNameSuffix, + + [Parameter(Mandatory)] + [String]$SecurityPrincipalNames, + + [Parameter(Mandatory=$false)] + [String]$SmbServerLocation, + + [Parameter(Mandatory=$false)] + [String]$StorageAccountPrefix, + + [Parameter(Mandatory=$false)] + [String]$StorageAccountResourceGroupName, + + [Parameter(Mandatory=$false)] + [Int]$StorageCount, + + [Parameter(Mandatory=$false)] + [Int]$StorageIndex, + + [Parameter(Mandatory)] + [String]$StorageService, + + [Parameter(Mandatory=$false)] + [String]$StorageSuffix, + + [Parameter(Mandatory=$false)] + [String]$SubscriptionId, + + [Parameter(Mandatory=$false)] + [String]$TenantId, + + [Parameter(Mandatory=$false)] + [String]$UserAssignedIdentityClientId +) + +function Write-Log +{ + param( + [parameter(Mandatory)] + [string]$Message, + + [parameter(Mandatory)] + [string]$Type + ) + $Path = 'C:\cse.txt' + if(!(Test-Path -Path $Path)) + { + New-Item -Path C:\ -Name cse.txt | Out-Null + } + $Timestamp = Get-Date -Format 'MM/dd/yyyy HH:mm:ss.ff' + $Entry = '[' + $Timestamp + '] [' + $Type + '] ' + $Message + $Entry | Out-File -FilePath $Path -Append +} + +$ErrorActionPreference = 'Stop' +$WarningPreference = 'SilentlyContinue' + +try +{ + ############################################################## + # Install Prerequisites + ############################################################## + # Install Active Directory PowerShell module + if($StorageService -eq 'AzureNetAppFiles' -or ($StorageService -eq 'AzureFiles' -and $ActiveDirectorySolution -eq 'ActiveDirectoryDomainServices')) + { + $RsatInstalled = (Get-WindowsFeature -Name 'RSAT-AD-PowerShell').Installed + if(!$RsatInstalled) + { + Install-WindowsFeature -Name 'RSAT-AD-PowerShell' | Out-Null + Write-Log -Message "Installation of the AD module succeeded" -Type 'INFO' + } + else + { + Write-Log -Message "AD module already exists" -Type 'INFO' + } + } + + + ############################################################## + # Variables + ############################################################## + # Convert Security Principal Names from a JSON array to a PowerShell array + [array]$SecurityPrincipalNames = $SecurityPrincipalNames.Replace("'",'"') | ConvertFrom-Json + Write-Log -Message "Security Principal Names:" -Type 'INFO' + $SecurityPrincipalNames | Add-Content -Path 'C:\cse.txt' -Force | Out-Null + + # Selects the appropraite share names based on the FslogixContainerType param from the deployment + $Shares = switch($FslogixContainerType) + { + 'CloudCacheProfileContainer' {@('profile-containers')} + 'CloudCacheProfileOfficeContainer' {@('office-containers','profile-containers')} + 'ProfileContainer' {@('profile-containers')} + 'ProfileOfficeContainer' {@('office-containers','profile-containers')} + } + + if($StorageService -eq 'AzureNetAppFiles' -or ($StorageService -eq 'AzureFiles' -and $ActiveDirectorySolution -eq 'ActiveDirectoryDomainServices')) + { + # Create Domain credential + $DomainUsername = $DomainJoinUserPrincipalName + $DomainPassword = ConvertTo-SecureString -String $DomainJoinPassword -AsPlainText -Force + [pscredential]$DomainCredential = New-Object System.Management.Automation.PSCredential ($DomainUsername, $DomainPassword) + + # Get Domain information + $Domain = Get-ADDomain -Credential $DomainCredential -Current 'LocalComputer' + Write-Log -Message "Domain information collection succeeded" -Type 'INFO' + } + + if($StorageService -eq 'AzureFiles') + { + $FilesSuffix = '.file.' + $StorageSuffix + Write-Log -Message "Azure Files Suffix = $FilesSuffix" -Type 'INFO' + } + + + ############################################################## + # Process Storage Resources + ############################################################## + for($i = 0; $i -lt $StorageCount; $i++) + { + # Determine Principal for assignment + $SecurityPrincipalName = $SecurityPrincipalNames[$i] + $Group = $Netbios + '\' + $SecurityPrincipalName + Write-Log -Message "Group for NTFS Permissions = $Group" -Type 'INFO' + + # Get storage resource details + switch($StorageService) + { + 'AzureNetAppFiles' { + $Credential = $DomainCredential + $SmbServerName = (Get-ADComputer -Filter "Name -like 'anf-$SmbServerLocation*'" -Credential $DomainCredential).Name + $FileServer = '\\' + $SmbServerName + '.' + $Domain.DNSRoot + } + 'AzureFiles' { + $StorageAccountName = $StorageAccountPrefix + ($i + $StorageIndex).ToString().PadLeft(2,'0') + $FileServer = '\\' + $StorageAccountName + $FilesSuffix + + # Connects to Azure using a User Assigned Managed Identity + Connect-AzAccount -Identity -AccountId $UserAssignedIdentityClientId -Environment $Environment -Tenant $TenantId -Subscription $SubscriptionId | Out-Null + Write-Log -Message "Authenticated to Azure" -Type 'INFO' + + # Get the storage account key + $StorageKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccountResourceGroupName -Name $StorageAccountName)[0].Value + Write-Log -Message "The GET operation for the Storage Account key on $StorageAccountName succeeded" -Type 'INFO' + + # Create credential for accessing the storage account + $StorageUsername = 'Azure\' + $StorageAccountName + $StoragePassword = ConvertTo-SecureString -String "$($StorageKey)" -AsPlainText -Force + [pscredential]$StorageKeyCredential = New-Object System.Management.Automation.PSCredential ($StorageUsername, $StoragePassword) + $Credential = $StorageKeyCredential + + if($ActiveDirectorySolution -eq 'ActiveDirectoryDomainServices') + { + # Get / create kerberos key for Azure Storage Account + $KerberosKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccountResourceGroupName -Name $StorageAccountName -ListKerbKey | Where-Object {$_.Keyname -contains 'kerb1'}).Value + if(!$KerberosKey) + { + New-AzStorageAccountKey -ResourceGroupName $StorageAccountResourceGroupName -Name $StorageAccountName -KeyName kerb1 | Out-Null + $Key = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccountResourceGroupName -Name $StorageAccountName -ListKerbKey | Where-Object {$_.Keyname -contains 'kerb1'}).Value + Write-Log -Message "Kerberos Key creation on Storage Account, $StorageAccountName, succeeded." -Type 'INFO' + } + else + { + $Key = $KerberosKey + Write-Log -Message "Acquired Kerberos Key from Storage Account, $StorageAccountName." -Type 'INFO' + } + + # Creates a password for the Azure Storage Account in AD using the Kerberos key + $ComputerPassword = ConvertTo-SecureString -String $Key.Replace("'","") -AsPlainText -Force + Write-Log -Message "Secure string conversion succeeded" -Type 'INFO' + + # Create the SPN value for the Azure Storage Account; attribute for computer object in AD + $SPN = 'cifs/' + $StorageAccountName + $FilesSuffix + + # Create the Description value for the Azure Storage Account; attribute for computer object in AD + $Description = "Computer account object for Azure storage account $($StorageAccountName)." + + # Create the AD computer object for the Azure Storage Account + $Computer = Get-ADComputer -Credential $DomainCredential -Filter {Name -eq $StorageAccountName} + if($Computer) + { + Remove-ADComputer -Credential $DomainCredential -Identity $StorageAccountName -Confirm:$false + } + $ComputerObject = New-ADComputer -Credential $DomainCredential -Name $StorageAccountName -Path $OrganizationalUnitPath -ServicePrincipalNames $SPN -AccountPassword $ComputerPassword -Description $Description -PassThru + Write-Log -Message "Computer object creation succeeded" -Type 'INFO' + + Set-AzStorageAccount ` + -ResourceGroupName $StorageAccountResourceGroupName ` + -Name $StorageAccountName ` + -EnableActiveDirectoryDomainServicesForFile $true ` + -ActiveDirectoryDomainName $Domain.DNSRoot ` + -ActiveDirectoryNetBiosDomainName $Domain.NetBIOSName ` + -ActiveDirectoryForestName $Domain.Forest ` + -ActiveDirectoryDomainGuid $Domain.ObjectGUID ` + -ActiveDirectoryDomainsid $Domain.DomainSID ` + -ActiveDirectoryAzureStorageSid $ComputerObject.SID.Value ` + -ActiveDirectorySamAccountName $StorageAccountName ` + -ActiveDirectoryAccountType 'Computer' | Out-Null + Write-Log -Message "Storage Account update with domain join info succeeded" -Type 'INFO' + + # Set the Kerberos encryption on the computer object + $DistinguishedName = 'CN=' + $StorageAccountName + ',' + $OrganizationalUnitPath + Set-ADComputer -Credential $DomainCredential -Identity $DistinguishedName -KerberosEncryptionType 'AES256' | Out-Null + Write-Log -Message "Setting Kerberos AES256 Encryption on the computer object succeeded" -Type 'INFO' + + # Reset the Kerberos key on the Storage Account + New-AzStorageAccountKey -ResourceGroupName $StorageAccountResourceGroupName -Name $StorageAccountName -KeyName kerb1 | Out-Null + $Key = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccountResourceGroupName -Name $StorageAccountName -ListKerbKey | Where-Object {$_.Keyname -contains 'kerb1'}).Value + Write-Log -Message "Resetting the Kerberos key on the Storage Account succeeded" -Type 'INFO' + + # Update the password on the computer object with the new Kerberos key on the Storage Account + $NewPassword = ConvertTo-SecureString -String $Key -AsPlainText -Force + Set-ADAccountPassword -Credential $DomainCredential -Identity $DistinguishedName -Reset -NewPassword $NewPassword | Out-Null + Write-Log -Message "Setting the new Kerberos key on the Computer Object succeeded" -Type 'INFO' + } + Disconnect-AzAccount | Out-Null + Write-Log -Message "Disconnection to Azure succeeded" -Type 'INFO' + } + } + + foreach($Share in $Shares) + { + # Mount file share + $FileShare = $FileServer + '\' + $Share + New-PSDrive -Name 'Z' -PSProvider 'FileSystem' -Root $FileShare -Credential $Credential | Out-Null + Write-Log -Message "Mounting the Azure file share, $FileShare, succeeded" -Type 'INFO' + + # Set recommended NTFS permissions on the file share + $ACL = Get-Acl -Path 'Z:' + $CreatorOwner = New-Object System.Security.Principal.Ntaccount ("Creator Owner") + $ACL.PurgeAccessRules($CreatorOwner) + $AuthenticatedUsers = New-Object System.Security.Principal.Ntaccount ("Authenticated Users") + $ACL.PurgeAccessRules($AuthenticatedUsers) + $Users = New-Object System.Security.Principal.Ntaccount ("Users") + $ACL.PurgeAccessRules($Users) + $DomainUsers = New-Object System.Security.AccessControl.FileSystemAccessRule("$Group","Modify","None","None","Allow") + $ACL.SetAccessRule($DomainUsers) + $CreatorOwner = New-Object System.Security.AccessControl.FileSystemAccessRule("Creator Owner","Modify","ContainerInherit,ObjectInherit","InheritOnly","Allow") + $ACL.AddAccessRule($CreatorOwner) + $ACL | Set-Acl -Path 'Z:' | Out-Null + Write-Log -Message "Setting the NTFS permissions on the Azure file share succeeded" -Type 'INFO' + + # Unmount file share + Remove-PSDrive -Name 'Z' -PSProvider 'FileSystem' -Force | Out-Null + Start-Sleep -Seconds 5 | Out-Null + Write-Log -Message "Unmounting the Azure file share, $FileShare, succeeded" -Type 'INFO' + } + } + $Output = [pscustomobject][ordered]@{ + shares = $Shares + } + $JsonOutput = $Output | ConvertTo-Json + return $JsonOutput +} +catch { + Write-Log -Message $_ -Type 'ERROR' + $ErrorData = $_ | Select-Object * + $ErrorData | Out-File -FilePath 'C:\cse.txt' -Append + throw +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-SessionHostConfiguration.ps1 b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-SessionHostConfiguration.ps1 new file mode 100644 index 000000000..d57c16fe6 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Set-SessionHostConfiguration.ps1 @@ -0,0 +1,535 @@ +[Cmdletbinding()] +Param( + [parameter(Mandatory)] + [string] + $ActiveDirectorySolution, + + [parameter(Mandatory)] + [string] + $AmdVmSize, + + [parameter(Mandatory)] + [string] + $AvdAgentBootLoaderMsiName, + + [parameter(Mandatory)] + [string] + $AvdAgentMsiName, + + [parameter(Mandatory)] + [string] + $Environment, + + [parameter(Mandatory)] + [string] + $Fslogix, + + [parameter(Mandatory)] + [string] + $FslogixContainerType, + + [parameter(Mandatory)] + [string] + $HostPoolName, + + [parameter(Mandatory)] + [string] + $HostPoolRegistrationToken, + + [parameter(Mandatory)] + [string] + $ImageOffer, + + [parameter(Mandatory)] + [string] + $ImagePublisher, + + [parameter(Mandatory)] + [string] + $NetAppFileShares, + + [parameter(Mandatory)] + [string] + $NvidiaVmSize, + + [parameter(Mandatory)] + [string] + $PooledHostPool, + + [parameter(Mandatory)] + [string] + $SecurityMonitoring, + + [parameter(Mandatory)] + [string] + $SecurityWorkspaceId, + + [parameter(Mandatory)] + [string] + $SecurityWorkspaceKey, + + [parameter(Mandatory)] + [string] + $StorageAccountPrefix, + + [parameter(Mandatory)] + [int] + $StorageCount, + + [parameter(Mandatory)] + [int] + $StorageIndex, + + [parameter(Mandatory)] + [string] + $StorageService, + + [parameter(Mandatory)] + [string] + $StorageSuffix +) + + +############################################################## +# Functions +############################################################## +function Write-Log +{ + param( + [parameter(Mandatory)] + [string]$Message, + + [parameter(Mandatory)] + [string]$Type + ) + $Path = 'C:\cse.txt' + if(!(Test-Path -Path $Path)) + { + New-Item -Path 'C:\' -Name 'cse.txt' | Out-Null + } + $Timestamp = Get-Date -Format 'MM/dd/yyyy HH:mm:ss.ff' + $Entry = '[' + $Timestamp + '] [' + $Type + '] ' + $Message + $Entry | Out-File -FilePath $Path -Append +} + +$ErrorActionPreference = 'Stop' +$WarningPreference = 'SilentlyContinue' + +try +{ + # Convert NetAppFiles share names from a JSON array to a PowerShell array + [array]$NetAppFileShares = $NetAppFileShares.Replace("'",'"') | ConvertFrom-Json + Write-Log -Message "Azure NetApp Files, Shares:" -Type 'INFO' + $NetAppFileShares | Add-Content -Path 'C:\cse.txt' -Force + + ############################################################## + # Add Recommended Security Settings + ############################################################## + $Settings = @( + + # Set Kerberos Encryption for STIG compliance + [PSCustomObject]@{ + Name = 'SupportedEncryptionTypes' + Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Kerberos\Parameters' + PropertyType = 'DWord' + Value = 2147483640 + } + ) + + ############################################################## + # Add Recommended AVD Settings + ############################################################## + $Settings = @( + + # Disable Automatic Updates: https://learn.microsoft.com/azure/virtual-desktop/set-up-customize-master-image#disable-automatic-updates + [PSCustomObject]@{ + Name = 'NoAutoUpdate' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' + PropertyType = 'DWord' + Value = 1 + }, + + # Enable Time Zone Redirection: https://learn.microsoft.com/azure/virtual-desktop/set-up-customize-master-image#set-up-time-zone-redirection + [PSCustomObject]@{ + Name = 'fEnableTimeZoneRedirection' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + } + ) + + + ############################################################## + # Add GPU Settings + ############################################################## + # This setting applies to the VM Size's recommended for AVD with a GPU + if ($AmdVmSize -eq 'true' -or $NvidiaVmSize -eq 'true') + { + $Settings += @( + + # Configure GPU-accelerated app rendering: https://learn.microsoft.com/azure/virtual-desktop/configure-vm-gpu#configure-gpu-accelerated-app-rendering + [PSCustomObject]@{ + Name = 'bEnumerateHWBeforeSW' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + }, + + # Configure fullscreen video encoding: https://learn.microsoft.com/azure/virtual-desktop/configure-vm-gpu#configure-fullscreen-video-encoding + [PSCustomObject]@{ + Name = 'AVC444ModePreferred' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + } + ) + } + + # This setting applies only to VM Size's recommended for AVD with a Nvidia GPU + if($NvidiaVmSize -eq 'true') + { + $Settings += @( + + # Configure GPU-accelerated frame encoding: https://learn.microsoft.com/azure/virtual-desktop/configure-vm-gpu#configure-gpu-accelerated-frame-encoding + [PSCustomObject]@{ + Name = 'AVChardwareEncodePreferred' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + } + ) + } + + + ############################################################## + # Add Fslogix Settings + ############################################################## + if($Fslogix -eq 'true') + { + $FilesSuffix = '.file.' + $StorageSuffix + $CloudCacheOfficeContainers = @() + $CloudCacheProfileContainers = @() + $OfficeContainers = @() + $ProfileContainers = @() + switch($StorageService) + { + 'AzureFiles' { + for($i = $StorageIndex; $i -lt $($StorageIndex + $StorageCount); $i++) + { + $CloudCacheOfficeContainers += 'type=smb,connectionString=\\' + $StorageAccountPrefix + $i.ToString().PadLeft(2,'0') + $FilesSuffix + '\office-containers;' + $CloudCacheProfileContainers += 'type=smb,connectionString=\\' + $StorageAccountPrefix + $i.ToString().PadLeft(2,'0') + $FilesSuffix + '\profile-containers;' + $OfficeContainers += '\\' + $StorageAccountPrefix + $i.ToString().PadLeft(2,'0') + $FilesSuffix + '\office-containers' + $ProfileContainers += '\\' + $StorageAccountPrefix + $i.ToString().PadLeft(2,'0') + $FilesSuffix + '\profile-containers' + } + } + 'AzureNetAppFiles' { + $CloudCacheOfficeContainers += 'type=smb,connectionString=\\' + $NetAppFileShares[0] + '\office-containers;' + $CloudCacheProfileContainers += 'type=smb,connectionString=\\' + $(if($NetAppFileShares.Length -gt 1){$NetAppFileShares[1]}else{$NetAppFileShares[0]}) + '\profile-containers;' + $OfficeContainers += '\\' + $NetAppFileShares[0] + '\office-containers' + $ProfileContainers += '\\' + $(if($NetAppFileShares.Length -gt 1){$NetAppFileShares[1]}else{$NetAppFileShares[0]}) + '\profile-containers' + } + } + + $Shares = @() + $Shares += $OfficeContainers + $Shares += $ProfileContainers + $SharesOutput = if($Shares.Count -eq 1){$Shares}else{$Shares -join ', '} + Write-Log -Message "File Shares: $SharesOutput" -Type 'INFO' + + $Settings += @( + + # Enables Fslogix profile containers: https://learn.microsoft.com/fslogix/profile-container-configuration-reference#enabled + [PSCustomObject]@{ + Name = 'Enabled' + Path = 'HKLM:\SOFTWARE\Fslogix\Profiles' + PropertyType = 'DWord' + Value = 1 + }, + + # Deletes a local profile if it exists and matches the profile being loaded from VHD: https://learn.microsoft.com/fslogix/profile-container-configuration-reference#deletelocalprofilewhenvhdshouldapply + [PSCustomObject]@{ + Name = 'DeleteLocalProfileWhenVHDShouldApply' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 1 + }, + + # The folder created in the Fslogix fileshare will begin with the username instead of the SID: https://learn.microsoft.com/fslogix/profile-container-configuration-reference#flipflopprofiledirectoryname + [PSCustomObject]@{ + Name = 'FlipFlopProfileDirectoryName' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 1 + }, + + # Specifies the number of retries attempted when a VHD(x) file is locked: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=profiles#lockedretrycount + [PSCustomObject]@{ + Name = 'LockedRetryCount' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 3 + }, + + # Specifies the number of seconds to wait between retries: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=profiles#lockedretryinterval + [PSCustomObject]@{ + Name = 'LockedRetryInterval' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 15 + }, + + # Specifies if the profile container can be accessed concurrently: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=profiles#profiletype + [PSCustomObject]@{ + Name = 'ProfileType' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 0 + }, + + # Specifies the number of seconds to wait between retries when attempting to reattach the VHD(x) container if it's disconnected unexpectedly: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=profiles#reattachintervalseconds + [PSCustomObject]@{ + Name = 'ReAttachIntervalSeconds' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 15 + }, + + # Specifies the number of times the system should attempt to reattach the VHD(x) container if it's disconnected unexpectedly: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=profiles#reattachretrycount + [PSCustomObject]@{ + Name = 'ReAttachRetryCount' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 3 + }, + + # Specifies the maximum size of the user's container in megabytes. Newly created VHD(x) containers are of this size: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=profiles#sizeinmbs + [PSCustomObject]@{ + Name = 'SizeInMBs' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 30000 + }, + + # Specifies the file extension for the profile containers: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=profiles#volumetype + [PSCustomObject]@{ + Name = 'VolumeType' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'String' + Value = 'VHDX' + } + ) + + if($FslogixContainerType -like "CloudCache*") + { + $Settings += @( + # List of file system locations to search for the user's profile VHD(X) file: https://learn.microsoft.com/fslogix/profile-container-configuration-reference#vhdlocations + [PSCustomObject]@{ + Name = 'CCDLocations' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'MultiString' + Value = $CloudCacheProfileContainers + } + ) + } + else + { + $Settings += @( + # List of file system locations to search for the user's profile VHD(X) file: https://learn.microsoft.com/fslogix/profile-container-configuration-reference#vhdlocations + [PSCustomObject]@{ + Name = 'VHDLocations' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'MultiString' + Value = $ProfileContainers + } + ) + } + + if($FslogixContainerType -like "*OfficeContainer") + { + $Settings += @( + + # Enables Fslogix office containers: https://learn.microsoft.com/fslogix/office-container-configuration-reference#enabled + [PSCustomObject]@{ + Name = 'Enabled' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'DWord' + Value = 1 + }, + + # The folder created in the Fslogix fileshare will begin with the username instead of the SID: https://learn.microsoft.com/fslogix/office-container-configuration-reference#flipflopprofiledirectoryname + [PSCustomObject]@{ + Name = 'FlipFlopProfileDirectoryName' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'DWord' + Value = 1 + }, + + # Teams data is redirected to the container: https://learn.microsoft.com/fslogix/office-container-configuration-reference#includeteams + [PSCustomObject]@{ + Name = 'IncludeTeams' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'DWord' + Value = 1 + }, + + # Specifies the number of retries attempted when a VHD(x) file is locked: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=odfc#lockedretrycount + [PSCustomObject]@{ + Name = 'LockedRetryCount' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'DWord' + Value = 3 + }, + + # Specifies the number of seconds to wait between retries: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=odfc#lockedretryinterval + [PSCustomObject]@{ + Name = 'LockedRetryInterval' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'DWord' + Value = 15 + }, + + # Specifies the number of seconds to wait between retries when attempting to reattach the VHD(x) container if it's disconnected unexpectedly: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=odfc#reattachintervalseconds + [PSCustomObject]@{ + Name = 'ReAttachIntervalSeconds' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'DWord' + Value = 15 + }, + + # Specifies the number of times the system should attempt to reattach the VHD(x) container if it's disconnected unexpectedly: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=odfc#reattachretrycount + [PSCustomObject]@{ + Name = 'ReAttachRetryCount' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'DWord' + Value = 3 + }, + + # Specifies the maximum size of the user's container in megabytes: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=odfc#sizeinmbs + [PSCustomObject]@{ + Name = 'SizeInMBs' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'DWord' + Value = 30000 + }, + + # Specifies the type of container: https://learn.microsoft.com/fslogix/reference-configuration-settings?tabs=odfc#volumetype + [PSCustomObject]@{ + Name = 'VolumeType' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'String' + Value = 'VHDX' + } + ) + + if($FslogixContainerType -like "CloudCache*") + { + $Settings += @( + # List of file system locations to search for the user's profile VHD(X) file: https://learn.microsoft.com/fslogix/profile-container-configuration-reference#vhdlocations + [PSCustomObject]@{ + Name = 'CCDLocations' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'MultiString' + Value = $CloudCacheOfficeContainers + } + ) + } + else + { + $Settings += @( + # List of file system locations to search for the user's profile VHD(X) file: https://learn.microsoft.com/fslogix/office-container-configuration-reference#vhdlocations + [PSCustomObject]@{ + Name = 'VHDLocations' + Path = 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' + PropertyType = 'MultiString' + Value = $OfficeContainers + } + ) + } + } + } + + + # Set registry settings + foreach($Setting in $Settings) + { + # Create registry key(s) if necessary + if(!(Test-Path -Path $Setting.Path)) + { + New-Item -Path $Setting.Path -Force | Out-Null + } + + # Checks for existing registry setting + $Value = Get-ItemProperty -Path $Setting.Path -Name $Setting.Name -ErrorAction 'SilentlyContinue' + $LogOutputValue = 'Path: ' + $Setting.Path + ', Name: ' + $Setting.Name + ', PropertyType: ' + $Setting.PropertyType + ', Value: ' + $Setting.Value + + # Creates the registry setting when it does not exist + if(!$Value) + { + New-ItemProperty -Path $Setting.Path -Name $Setting.Name -PropertyType $Setting.PropertyType -Value $Setting.Value -Force | Out-Null + Write-Log -Message "Added registry setting: $LogOutputValue" -Type 'INFO' + } + # Updates the registry setting when it already exists + elseif($Value.$($Setting.Name) -ne $Setting.Value) + { + Set-ItemProperty -Path $Setting.Path -Name $Setting.Name -Value $Setting.Value -Force | Out-Null + Write-Log -Message "Updated registry setting: $LogOutputValue" -Type 'INFO' + } + # Writes log output when registry setting has the correct value + else + { + Write-Log -Message "Registry setting exists with correct value: $LogOutputValue" -Type 'INFO' + } + Start-Sleep -Seconds 1 | Out-Null + } + + + ############################################################## + # Install the AVD Agent + ############################################################## + Start-Process -FilePath 'msiexec.exe' -ArgumentList "/i `"$AvdAgentBootLoaderMsiName`" /quiet /qn /norestart /passive" -Wait -Passthru | Out-Null + Write-Log -Message 'Installed AVD Agent Bootloader' -Type 'INFO' + Start-Sleep -Seconds 5 | Out-Null + + Start-Process -FilePath 'msiexec.exe' -ArgumentList "/i `"$AvdAgentMsiName`" /quiet /qn /norestart /passive REGISTRATIONTOKEN=$HostPoolRegistrationToken" -Wait -PassThru | Out-Null + Write-Log -Message 'Installed AVD Agent' -Type 'INFO' + Start-Sleep -Seconds 5 | Out-Null + + + ############################################################## + # Dual-home Microsoft Monitoring Agent for Azure Sentinel or Defender for Cloud + ############################################################## + if($SecurityMonitoring -eq 'true') + { + $AzureEnvironment = switch($Environment) + { + AzureCloud {0} + AzureUSGovernment {1} + AzureChina {2} + USNat {3} + USSec {4} + } + + $mma = New-Object -ComObject 'AgentConfigManager.MgmtSvcCfg' + $mma.AddCloudWorkspace($SecurityWorkspaceId, $SecurityWorkspaceKey, $AzureEnvironment) + $mma.ReloadConfiguration() | Out-Null + } + + ############################################################## + # Restart VM + ############################################################## + if(($ActiveDirectorySolution -eq "MicrosoftEntraId" -or $ActiveDirectorySolution -eq "MicrosoftEntraIdIntuneEnrollment") -and $AmdVmSize -eq 'false' -and $NvidiaVmSize -eq 'false') + { + Start-Process -FilePath 'shutdown' -ArgumentList '/r /t 30' | Out-Null + } + + $Output = [pscustomobject][ordered]@{ + activeDirectorySolution = $ActiveDirectorySolution + } + $JsonOutput = $Output | ConvertTo-Json + return $JsonOutput +} +catch +{ + Write-Log -Message $_ -Type 'ERROR' + throw +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/artifacts/Update-AvdDesktop.ps1 b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Update-AvdDesktop.ps1 new file mode 100644 index 000000000..0a7fcb40f --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Update-AvdDesktop.ps1 @@ -0,0 +1,53 @@ +Param( + [parameter(Mandatory)] + [string]$ApplicationGroupName, + + [parameter(Mandatory)] + [string]$Environment, + + [parameter(Mandatory)] + [string]$FriendlyName, + + [parameter(Mandatory)] + [string]$ResourceGroupName, + + [parameter(Mandatory)] + [string]$SubscriptionId, + + [parameter(Mandatory)] + [string]$TenantId, + + [parameter(Mandatory)] + [string]$UserAssignedIdentityClientId +) + +$ErrorActionPreference = 'Stop' + +try +{ + Connect-AzAccount ` + -Environment $Environment ` + -Tenant $TenantId ` + -Subscription $SubscriptionId ` + -Identity ` + -AccountId $UserAssignedIdentityClientId | Out-Null + + Update-AzWvdDesktop ` + -ApplicationGroupName $ApplicationGroupName ` + -Name 'SessionDesktop' ` + -ResourceGroupName $ResourceGroupName ` + -FriendlyName $FriendlyName.Replace('"', '') | Out-Null + + Disconnect-AzAccount | Out-Null + + $Output = [pscustomobject][ordered]@{ + applicationGroupName = $ApplicationGroupName + } + $JsonOutput = $Output | ConvertTo-Json + return $JsonOutput +} +catch +{ + Write-Host $_ | Select-Object * + throw +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/artifacts/Update-AvdWorkspace.ps1 b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Update-AvdWorkspace.ps1 new file mode 100644 index 000000000..4b0b86e6b --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/artifacts/Update-AvdWorkspace.ps1 @@ -0,0 +1,59 @@ +param( + + [parameter(Mandatory)] + [string]$ApplicationGroupReferences, + + [parameter(Mandatory)] + [string]$Environment, + + [parameter(Mandatory)] + [string]$ResourceGroupName, + + [parameter(Mandatory)] + [string]$SubscriptionId, + + [parameter(Mandatory)] + [string]$TenantId, + + [parameter(Mandatory)] + [string]$UserAssignedIdentityClientId, + + [parameter(Mandatory)] + [string]$WorkspaceName + +) + +$ErrorActionPreference = 'Stop' + +try +{ + Connect-AzAccount ` + -Environment $Environment ` + -Tenant $TenantId ` + -Subscription $SubscriptionId ` + -Identity ` + -AccountId $UserAssignedIdentityClientId | Out-Null + + $OldAppGroupReferences = (Get-AzWvdWorkspace -ResourceGroupName $ResourceGroupName -Name $WorkspaceName).ApplicationGroupReference + [array]$NewAppGroupReferences = $ApplicationGroupReferences.Replace("'",'"') | ConvertFrom-Json + $OldAppGroupReferences = $OldAppGroupReferences -ne $NewAppGroupReferences + $CombinedApplicationGroupReferences = $OldAppGroupReferences + $NewAppGroupReferences + + Update-AzWvdWorkspace ` + -ResourceGroupName $ResourceGroupName ` + -Name $WorkspaceName ` + -ApplicationGroupReference $CombinedApplicationGroupReferences | Out-Null + + Disconnect-AzAccount | Out-Null + + $Output = [pscustomobject][ordered]@{ + workspaceName = $WorkspaceName + } + $JsonOutput = $Output | ConvertTo-Json + return $JsonOutput +} +catch +{ + Write-Host $_ | Select-Object * + throw +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/data/locations.json b/src/bicep/add-ons/azureVirtualDesktop/data/locations.json new file mode 100644 index 000000000..06f20c9e5 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/data/locations.json @@ -0,0 +1,396 @@ +{ + "AzureChina": { + "chinaeast": { + "abbreviation": "cne", + "recoveryServicesGeo": "sha", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + }, + "chinaeast2": { + "abbreviation": "cne2", + "recoveryServicesGeo": "sha2", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + }, + "chinanorth": { + "abbreviation": "cnn", + "recoveryServicesGeo": "bjb", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + }, + "chinanorth2": { + "abbreviation": "cnn2", + "recoveryServicesGeo": "bjb2", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + }, + "chinanorth3": { + "abbreviation": "cnn3", + "recoveryServicesGeo": "", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + } + }, + "AzureCloud": { + "australiacentral": { + "abbreviation": "auc", + "recoveryServicesGeo": "acl", + "timeDifference": "+10:00", + "timeZone": "AUS Eastern Standard Time" + }, + "australiacentral2": { + "abbreviation": "auc2", + "recoveryServicesGeo": "acl2", + "timeDifference": "+10:00", + "timeZone": "AUS Eastern Standard Time" + }, + "australiaeast": { + "abbreviation": "aue", + "recoveryServicesGeo": "ae", + "timeDifference": "+10:00", + "timeZone": "AUS Eastern Standard Time" + }, + "australiasoutheast": { + "abbreviation": "ause", + "recoveryServicesGeo": "ase", + "timeDifference": "+10:00", + "timeZone": "AUS Eastern Standard Time" + }, + "brazilsouth": { + "abbreviation": "brs", + "recoveryServicesGeo": "brs", + "timeDifference": "-3:00", + "timeZone": "E. South America Standard Time" + }, + "brazilsoutheast": { + "abbreviation": "brse", + "recoveryServicesGeo": "bse", + "timeDifference": "-3:00", + "timeZone": "E. South America Standard Time" + }, + "canadacentral": { + "abbreviation": "cac", + "recoveryServicesGeo": "cnc", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "canadaeast": { + "abbreviation": "cae", + "recoveryServicesGeo": "cne", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "centralindia": { + "abbreviation": "inc", + "recoveryServicesGeo": "inc", + "timeDifference": "+5:30", + "timeZone": "India Standard Time" + }, + "centralus": { + "abbreviation": "usc", + "recoveryServicesGeo": "cus", + "timeDifference": "-6:00", + "timeZone": "Central Standard Time" + }, + "eastasia": { + "abbreviation": "ase", + "recoveryServicesGeo": "ea", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + }, + "eastus": { + "abbreviation": "use", + "recoveryServicesGeo": "eus", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "eastus2": { + "abbreviation": "use2", + "recoveryServicesGeo": "eus2", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "francecentral": { + "abbreviation": "frc", + "recoveryServicesGeo": "frc", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "francesouth": { + "abbreviation": "frs", + "recoveryServicesGeo": "frs", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "germanynorth": { + "abbreviation": "den", + "recoveryServicesGeo": "gn", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "germanywestcentral": { + "abbreviation": "dewc", + "recoveryServicesGeo": "gwc", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "israelcentral": { + "abbreviation": "ilc", + "recoveryServicesGeo": "ilc", + "timeDifference": "+2:00", + "timeZone": "Israel Standard Time" + }, + "italynorth": { + "abbreviation": "itn", + "recoveryServicesGeo": "itn", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "japaneast": { + "abbreviation": "jpe", + "recoveryServicesGeo": "jpe", + "timeDifference": "+9:00", + "timeZone": "Tokyo Standard Time" + }, + "japanwest": { + "abbreviation": "jpw", + "recoveryServicesGeo": "jpw", + "timeDifference": "+9:00", + "timeZone": "Tokyo Standard Time" + }, + "jioindiacentral": { + "abbreviation": "injc", + "recoveryServicesGeo": "jic", + "timeDifference": "+5:30", + "timeZone": "India Standard Time" + }, + "jioindiawest": { + "abbreviation": "injw", + "recoveryServicesGeo": "jiw", + "timeDifference": "+5:30", + "timeZone": "India Standard Time" + }, + "koreacentral": { + "abbreviation": "krc", + "recoveryServicesGeo": "krc", + "timeDifference": "+9:00", + "timeZone": "Korea Standard Time" + }, + "koreasouth": { + "abbreviation": "krs", + "recoveryServicesGeo": "krs", + "timeDifference": "+9:00", + "timeZone": "Korea Standard Time" + }, + "newzealandnorth": { + "abbreviation": "nzn", + "recoveryServicesGeo": "", + "timeDifference": "+13:00", + "timeZone": "New Zealand Standard Time" + }, + "northcentralus": { + "abbreviation": "usnc", + "recoveryServicesGeo": "ncus", + "timeDifference": "-6:00", + "timeZone": "Central Standard Time" + }, + "northeurope": { + "abbreviation": "eun", + "recoveryServicesGeo": "ne", + "timeDifference": "0:00", + "timeZone": "GMT Standard Time" + }, + "norwayeast": { + "abbreviation": "noe", + "recoveryServicesGeo": "nwe", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "norwaywest": { + "abbreviation": "now", + "recoveryServicesGeo": "nww", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "polandcentral": { + "abbreviation": "plc", + "recoveryServicesGeo": "plc", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "qatarcentral": { + "abbreviation": "qac", + "recoveryServicesGeo": "qac", + "timeDifference": "+3:00", + "timeZone": "Arabian Standard Time" + }, + "southafricanorth": { + "abbreviation": "zan", + "recoveryServicesGeo": "san", + "timeDifference": "+2:00", + "timeZone": "South Africa Standard Time" + }, + "southafricawest": { + "abbreviation": "zaw", + "recoveryServicesGeo": "saw", + "timeDifference": "+2:00", + "timeZone": "South Africa Standard Time" + }, + "southcentralus": { + "abbreviation": "ussc", + "recoveryServicesGeo": "scus", + "timeDifference": "-6:00", + "timeZone": "Central Standard Time" + }, + "southeastasia": { + "abbreviation": "asse", + "recoveryServicesGeo": "sea", + "timeDifference": "+8:00", + "timeZone": "Singapore Standard Time" + }, + "southindia": { + "abbreviation": "ins", + "recoveryServicesGeo": "ins", + "timeDifference": "+5:30", + "timeZone": "India Standard Time" + }, + "swedencentral": { + "abbreviation": "sec", + "recoveryServicesGeo": "sdc", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "switzerlandnorth": { + "abbreviation": "chn", + "recoveryServicesGeo": "szn", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "switzerlandwest": { + "abbreviation": "chw", + "recoveryServicesGeo": "szw", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "uaecentral": { + "abbreviation": "aec", + "recoveryServicesGeo": "uac", + "timeDifference": "+3:00", + "timeZone": "Arabian Standard Time" + }, + "uaenorth": { + "abbreviation": "aen", + "recoveryServicesGeo": "uan", + "timeDifference": "+3:00", + "timeZone": "Arabian Standard Time" + }, + "uksouth": { + "abbreviation": "uks", + "recoveryServicesGeo": "uks", + "timeDifference": "0:00", + "timeZone": "GMT Standard Time" + }, + "ukwest": { + "abbreviation": "ukw", + "recoveryServicesGeo": "ukw", + "timeDifference": "0:00", + "timeZone": "GMT Standard Time" + }, + "westcentralus": { + "abbreviation": "uswc", + "recoveryServicesGeo": "wcus", + "timeDifference": "-7:00", + "timeZone": "Mountain Standard Time" + }, + "westeurope": { + "abbreviation": "euw", + "recoveryServicesGeo": "we", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "westindia": { + "abbreviation": "inw", + "recoveryServicesGeo": "inw", + "timeDifference": "+5:30", + "timeZone": "India Standard Time" + }, + "westus": { + "abbreviation": "usw", + "recoveryServicesGeo": "wus", + "timeDifference": "-8:00", + "timeZone": "Pacific Standard Time" + }, + "westus2": { + "abbreviation": "usw2", + "recoveryServicesGeo": "wus2", + "timeDifference": "-8:00", + "timeZone": "Pacific Standard Time" + }, + "westus3": { + "abbreviation": "usw3", + "recoveryServicesGeo": "wus3", + "timeDifference": "-7:00", + "timeZone": "Mountain Standard Time" + } + }, + "AzureUSGovernment": { + "usdodcentral": { + "abbreviation": "dodc", + "recoveryServicesGeo": "udc", + "timeDifference": "-6:00", + "timeZone": "Central Standard Time" + }, + "usdodeast": { + "abbreviation": "dode", + "recoveryServicesGeo": "ude", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "usgovarizona": { + "abbreviation": "az", + "recoveryServicesGeo": "uga", + "timeDifference": "-7:00", + "timeZone": "Mountain Standard Time" + }, + "usgovtexas": { + "abbreviation": "tx", + "recoveryServicesGeo": "ugt", + "timeDifference": "-6:00", + "timeZone": "Central Standard Time" + }, + "usgovvirginia": { + "abbreviation": "va", + "recoveryServicesGeo": "ugv", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + } + }, + "USNat": { + "usnateast": { + "abbreviation": "east", + "recoveryServicesGeo": "exe", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "usnatwest": { + "abbreviation": "west", + "recoveryServicesGeo": "exw", + "timeDifference": "-8:00", + "timeZone": "Pacific Standard Time" + } + }, + "USSec": { + "usseceast": { + "abbreviation": "east", + "recoveryServicesGeo": "rxe", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "ussecwest": { + "abbreviation": "west", + "recoveryServicesGeo": "rxw", + "timeDifference": "-8:00", + "timeZone": "Pacific Standard Time" + } + } +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/data/resourceAbbreviations.json b/src/bicep/add-ons/azureVirtualDesktop/data/resourceAbbreviations.json new file mode 100644 index 000000000..c2ebdf046 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/data/resourceAbbreviations.json @@ -0,0 +1,26 @@ +{ + "automationAccounts": "aa", + "availabilitySets": "as", + "dataCollectionRuleAssociations": "dcra", + "dataCollectionRules": "dcr", + "desktopApplicationGroups": "dag", + "diskAccesses": "da", + "remoteApplicationGroups": "rag", + "disks": "disk", + "diskEncryptionSets": "des", + "hostPools": "hp", + "keyVaults": "kv", + "logAnalyticsWorkspaces": "law", + "netAppAccounts": "naa", + "netAppCapacityPools": "nacp", + "networkInterfaces": "nic", + "networkSecurityGroups": "nsg", + "recoveryServicesVaults": "rsv", + "resourceGroups": "rg", + "routeTables": "rt", + "storageAccounts": "sa", + "userAssignedIdentities": "uai", + "virtualMachines": "vm", + "virtualNetworks": "vnet", + "workspaces": "ws" +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/design.md b/src/bicep/add-ons/azureVirtualDesktop/docs/design.md new file mode 100644 index 000000000..54eccdda9 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/design.md @@ -0,0 +1,23 @@ +# Azure Virtual Desktop Solution + +[**Home**](../README.md) | [**Features**](./features.md) | [**Design**](./design.md) | [**Prerequisites**](./prerequisites.md) | [**Troubleshooting**](./troubleshooting.md) + +## Design + +This Azure Virtual Desktop (AVD) solution will deploy a fully operational AVD [stamp](https://learn.microsoft.com/azure/architecture/patterns/deployment-stamp) in an Azure subscription. The "StampIndex" parameter in this solution allows each stamp to be identified and scale to the capacity of a single subscription. Either several small stamps or one large stamp could be deployed in one subscription. To uniquely name multiple, unrelated stamps within a subscription, input a unique value for the "Identifier" parameter in each deployment. To name multiple related stamps, use the same value for the "Identifier" but increment the "StampIndex" across your subscriptions. + +![Identifiers](../images/identifiers.png) + +Every AVD deployment within the same subscription will share the AVD global workspace. When the same "Identifier" is used but the "StampIndex" is incremented across the same subscription, the feed workspace is deployed only once per "Identifier". This simplifies the organization of AVD resources within the client. The "Identfier" is meant to represent a business unit or project within an organization. + +![Stamps](../images/stamps.png) + +The code is idempotent, allowing you to scale storage and sessions hosts, but the core management resources will persist and update for any subsequent deployments. Some of those resources are the host pool, application group, and log analytics workspace. + +Both a personal or pooled host pool can be deployed with this solution. Either option will deploy a desktop application group with a role assignment. Selecting a pooled host pool will deploy the required resources and configurations to fully enable FSLogix. This solution also automates many of the features that are usually enabled manually after deploying an AVD host pool. See the [features](./features.md) page for more details. + +With this solution you can scale up to Azure's subscription limitations. This solution has been updated to allow sharding. A shard provides additional capacity to an AVD stamp. See the details below for increasing storage capacity. + +## Sharding to Increase Storage Capacity + +To add storage capacity to an AVD stamp, the "StorageIndex" and "StorageCount" parameters should be modified to your desired capacity. The last two digits in the name for the chosen storage solution will be incremented between each deployment. The "VHDLocations" setting will include all the file shares. The "SecurityPrincipalIds" and "SecurityPrincipalNames" will have an RBAC assignment and NTFS permissions set on one storage shard per stamp. Each user in the stamp should only have access to one file share. When the user accesses a session host, their profile will load from their respective file share. diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/design/naming.md b/src/bicep/add-ons/azureVirtualDesktop/docs/design/naming.md new file mode 100644 index 000000000..ea9803a65 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/design/naming.md @@ -0,0 +1,5 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Naming Convention diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features.md new file mode 100644 index 000000000..e8e997b56 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features.md @@ -0,0 +1,19 @@ +# Azure Virtual Desktop Solution + +[**Home**](../README.md) | [**Features**](./features.md) | [**Design**](./design.md) | [**Prerequisites**](./prerequisites.md) | [**Troubleshooting**](./troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./features/autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./features/backups.md#backups) +- [**Drain Mode**](./features/drainMode.md#drain-mode) +- [**FSLogix**](./features/fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./features/gpu.md#gpu-drivers--settings) +- [**High Availability**](./features/highAvailability.md#high-availability) +- [**Monitoring**](./features/monitoring.md#monitoring) +- [**Scaling Tool**](./features/scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./features/serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./features/smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./features/startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./features/trustedLaunch.md#trusted-launch) +- [**Validation**](./features/validation.md#validation) \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/autoIncreasePremiumFileShareQuota.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/autoIncreasePremiumFileShareQuota.md new file mode 100644 index 000000000..fa613522e --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/autoIncreasePremiumFileShareQuota.md @@ -0,0 +1,35 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### Auto Increase Premium File Share Quota + +When Azure Files Premium is selected for FSLogix Storage, this feature is deployed automatically. This tool helps reduce cost by scaling the file share quota only when needed. To benefit from the cost savings, select 100GB for your initial file share size. For the first 500GB, the share will scale up 100 GB when only 50GB of quota remains. Once the share has reached 500GB, the tool will scale up 500GB if less than 500GB of the quota remains. + +**Reference:** [Azure Samples - GitHub Repository](https://github.com/Azure-Samples/azure-files-samples/tree/master/autogrow-PFS-quota) + +**Deployed Resources:** + +- Automation Account + - Diagnositics Setting (optional) + - Job Schedules + - Runbook + - Schedules + - System Assigned Identity +- Role Assignment diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/backups.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/backups.md new file mode 100644 index 000000000..db7d05855 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/backups.md @@ -0,0 +1,32 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### Backups + +This optional feature enables backups to protect user profile data. When selected, if the host pool is "pooled" and the storage solution is Azure Files, the solution will protect the file share. If the host pool is "personal", the solution will protect the virtual machines. + +**Reference:** [Azure Backup - Microsoft Docs](https://docs.microsoft.com/en-us/azure/backup/backup-overview) + +**Deployed Resources:** + +- Recovery Services Vault +- Backup Policy +- Protection Container (File Share Only) +- Protected Item diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/drainMode.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/drainMode.md new file mode 100644 index 000000000..dcce7240a --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/drainMode.md @@ -0,0 +1,30 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### Drain Mode + +When this optional feature is deployed, the sessions hosts will be put in drain mode to ensure the end users cannot access them until they have been validated. + +**Reference:** [Drain Mode - Microsoft Docs](https://docs.microsoft.com/en-us/azure/virtual-desktop/drain-mode) + +**Deployed Resources:** + +- Virtual Machine + - Custom Script Extension diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/fslogix.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/fslogix.md new file mode 100644 index 000000000..5b878fcbe --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/fslogix.md @@ -0,0 +1,44 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### FSLogix + +If selected, this solution will deploy the required resources and configurations so that FSLogix is fully configured and ready for immediate use post deployment. Only Azure AD DS and AD DS are supported in this solution. Azure AD support is in "Public Preview" and will added after it is "Generally Available". Azure Files and Azure NetApp Files are the only two SMB storage services available in this solution. A management VM is deployed to facilitate the domain join of Azure Files (AD DS only) and configures the NTFS permissions on the share(s). Azure Files can be deployed with either a public endpoint, [service endpoint](https://docs.microsoft.com/en-us/azure/storage/files/storage-files-networking-overview#public-endpoint-firewall-settings), or [private endpoint](https://docs.microsoft.com/en-us/azure/storage/files/storage-files-networking-overview#private-endpoints). With this solution, FSLogix containers can be configured in multiple ways: + +- Cloud Cache Profile Container +- Cloud Cache Profile & Office Container +- Profile Container (Recommended) +- Profile & Office Container + +**Reference:** [FSLogix - Microsoft Docs](https://docs.microsoft.com/en-us/fslogix/overview) + +**Deployed Resources:** + +- Azure Storage Account (Optional) + - File Services + - Share(s) +- Azure NetApp Account (Optional) + - Capacity Pool + - Volume(s) +- Virtual Machine +- Network Interface +- Disk +- Private Endpoint (Optional) +- Private DNS Zone (Optional) diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/gpu.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/gpu.md new file mode 100644 index 000000000..d9ffc8a2a --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/gpu.md @@ -0,0 +1,32 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### GPU Drivers & Settings + +When an appropriate VM size (Nv, Nvv3, Nvv4, or NCasT4_v3 series) is selected, this solution will automatically deploy the appropriate virtual machine extension to install the graphics driver and configure the recommended registry settings. + +**Reference:** [Configure GPU Acceleration - Microsoft Docs](https://docs.microsoft.com/en-us/azure/virtual-desktop/configure-vm-gpu) + +**Deployed Resources:** + +- Virtual Machines Extensions + - AmdGpuDriverWindows + - NvidiaGpuDriverWindows + - CustomScriptExtension diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/highAvailability.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/highAvailability.md new file mode 100644 index 000000000..a359b7380 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/highAvailability.md @@ -0,0 +1,29 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### High Availability + +This optional feature will deploy the selected availability option and only provides high availability for "pooled" host pools since it is a load balanced solution. Virtual machines can be deployed in either Availability Zones or Availability Sets, to provide a higher SLA for your solution. SLA: 99.99% for Availability Zones, 99.95% for Availability Sets. + +**Reference:** [Availability options for Azure Virtual Machines - Microsoft Docs](https://docs.microsoft.com/en-us/azure/virtual-machines/availability) + +**Deployed Resources:** + +- Availability Set(s) (Optional) diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/monitoring.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/monitoring.md new file mode 100644 index 000000000..5e3d33c69 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/monitoring.md @@ -0,0 +1,35 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### Monitoring + +This feature deploys the required resources to enable the Insights workbook in the Azure Virtual Desktop blade in the Azure Portal. + +**Reference:** [Azure Monitor for AVD - Microsoft Docs](https://docs.microsoft.com/en-us/azure/virtual-desktop/azure-monitor) + +**Deployed Resources:** + +- Log Analytics Workspace + - Windows Events + - Performance Counters +- Microsoft Monitoring Agent extension +- Diagnostic Settings + - Host Pool + - Workspace diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/scalingTool.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/scalingTool.md new file mode 100644 index 000000000..505f5401a --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/scalingTool.md @@ -0,0 +1,35 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### Scaling Tool + +This feature is automatically deployed if a "pooled" host pool is selected to help save on cost. Based on the desired configuration, session hosts will scale up during peak hours and shutdown after peak hours. It is recommended to use policies to manage idle and disconnected over using the built-in capability in this tool. In this solution, a managed identity is deployed on the Automation Account to reduce the privileges needed for tool. + +**Reference:** [Scaling Tool - Microsoft Docs](https://docs.microsoft.com/en-us/azure/virtual-desktop/scaling-automation-logic-apps) + +**Deployed Resources:** + +- Automation Account + - Diagnostic Setting (optional) + - Job Schedules + - Runbook + - Schedules + - System Assigned Identity +- Role Assignment diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/serverSideEncryption.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/serverSideEncryption.md new file mode 100644 index 000000000..6bc21e374 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/serverSideEncryption.md @@ -0,0 +1,34 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### Server-Side Encryption with Customer Managed Keys + +This optional feature deploys the required resources & configuration to enable server-side encryption encryption on the session hosts using a customer managed key. The configuration also enables double encryption which uses a platform managed key in combination with the customer managed key. Also, the temp and cache disks are encrypted using the "encryption at host" feature. + +> **NOTE** +> If deploying a "pooled" host pool with FSLogix, the data in the profile and office containers are encrypted using encryption on the storage service, not the virtual machine. + +**Reference:** [Azure Server-Side Encryption - Microsoft Docs](https://learn.microsoft.com/azure/virtual-machines/disk-encryption) + +**Deployed Resources:** + +- Key Vault + - Key Encryption Key +- Disk Encryption Set diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/smbMultiChannel.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/smbMultiChannel.md new file mode 100644 index 000000000..91a5e0db4 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/smbMultiChannel.md @@ -0,0 +1,25 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### SMB Multichannel + +This feature is automatically enabled when Azure Files Premium is selected for FSLogix storage. This feature is only supported with Azure Files Premium and it allows multiple connections to an SMB share from an SMB client. + +**Reference:** [SMB Multichannel Performance - Microsoft Docs](https://docs.microsoft.com/en-us/azure/storage/files/storage-files-smb-multichannel-performance) diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/startVmOnConnect.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/startVmOnConnect.md new file mode 100644 index 000000000..cefb92fd5 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/startVmOnConnect.md @@ -0,0 +1,30 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### Start VM On Connect + +This optional feature allows your end users to turn on a session host when all the session hosts have been stopped / deallocated. This is done automatically when the end user opens the AVD client and attempts to access a resource. Start VM On Connect compliments scaling solutions by ensuring the session hosts can be turned off to reduce cost but made available when needed. + +**Reference:** [Start VM On Connect - Microsoft Docs](https://docs.microsoft.com/en-us/azure/virtual-desktop/start-virtual-machine-connect?tabs=azure-portal) + +**Deployed Resources:** + +- Role Assignment +- Host Pool diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/trustedLaunch.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/trustedLaunch.md new file mode 100644 index 000000000..3c6ff14ec --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/trustedLaunch.md @@ -0,0 +1,39 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### Trusted Launch + +This feature is enabled automatically with the safe boot and vTPM settings when the following conditions are met: + +- a generation 2, "g2", image SKU is selected +- the VM size supports the feature + +It is a security best practice to enable this feature to protect your virtual machines from: + +- boot kits +- rootkits +- kernel-level malware + +**Reference:** [Trusted Launch - Microsoft Docs](https://docs.microsoft.com/en-us/azure/virtual-machines/trusted-launch) + +**Deployed Resources:** + +- Virtual Machines + - Guest Attestation extension diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/features/validation.md b/src/bicep/add-ons/azureVirtualDesktop/docs/features/validation.md new file mode 100644 index 000000000..72a54046c --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/features/validation.md @@ -0,0 +1,31 @@ +# Azure Virtual Desktop Solution + +[**Home**](../../README.md) | [**Features**](../features.md) | [**Design**](../design.md) | [**Prerequisites**](../prerequisites.md) | [**Troubleshooting**](../troubleshooting.md) + +## Features + +- [**Auto Increase Premium File Share Quota**](./autoIncreasePremiumFileShareQuota.md#auto-increase-premium-file-share-quota) +- [**Backups**](./backups.md#backups) +- [**Drain Mode**](./drainMode.md#drain-mode) +- [**FSLogix**](./fslogix.md#fslogix) +- [**GPU Drivers & Settings**](./gpu.md#gpu-drivers--settings) +- [**High Availability**](./highAvailability.md#high-availability) +- [**Monitoring**](./monitoring.md#monitoring) +- [**Scaling Tool**](./scalingTool.md#scaling-tool) +- [**Server-Side Encryption with Customer Managed Keys**](./serverSideEncryption.md#server-side-encryption) +- [**SMB Multichannel**](./smbMultiChannel.md#smb-multichannel) +- [**Start VM On Connect**](./startVmOnConnect.md#start-vm-on-connect) +- [**Trusted Launch**](./trustedLaunch.md#trusted-launch) +- [**Validation**](./validation.md#validation) + +### Validation + +This feature will validate your deployment selections to ensure you are deploying a compliant configuration that follows Microsoft's best practices and collect information needed for the deployment. + +**Deployed Resources:** + +- Virtual Machine + - Custom Script Extension + - Disk + - Network Interface +- Role Assignments diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/prerequisites.md b/src/bicep/add-ons/azureVirtualDesktop/docs/prerequisites.md new file mode 100644 index 000000000..55a8e3beb --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/prerequisites.md @@ -0,0 +1,60 @@ +# Azure Virtual Desktop Solution + +[**Home**](../README.md) | [**Features**](./features.md) | [**Design**](./design.md) | [**Prerequisites**](./prerequisites.md) | [**Troubleshooting**](./troubleshooting.md) + +## Prerequisites + +To successfully deploy this solution, you will need to ensure the following prerequisites have been completed: + +### Required + +- **Licenses:** ensure you have the [required licensing for AVD](https://learn.microsoft.com/en-us/azure/virtual-desktop/overview#requirements). +- **Artifacts:** the deployment of this solution depends on many artifacts that must be hosted in Azure Blobs, ideally in the MLZ operations storage account. + - [AVD Agent](https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrmXv) + - [AVD Agent Boot Loader](https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrxrH) + - [Azure PowerShell AZ Module](https://github.com/Azure/azure-powershell/releases/download/v10.2.0-August2023/Az-Cmdlets-10.2.0.37547-x64.msi) + - [PowerShell Scripts](https://github.com/jamasten/AzureVirtualDesktop/tree/main/artifacts) +- **Azure Permissions:** ensure the principal deploying the solution has "Owner" and "Key Vault Administrator" roles assigned on the target Azure subscription. This solution contains many role assignments at different scopes so the principal deploying this solution will need to be an Owner at the subscription scope for a successful deployment. It also deploys keys and secrets in a key vault to enhance security. +- **Security Group:** create a security group for your AVD users. + - AD DS: create the group in ADUC and ensure the group has synchronized to Azure AD. + - Azure AD: create the group. + - Azure AD DS: create the group in Azure AD and ensure the group has synchronized to Azure AD DS. +- **Disk Encryption:** the "encryption at host" feature is deployed on the virtual machines to meet Zero Trust compliance. This feature is not enabled in your Azure subscription by default and must be manually enabled. Use the following steps to enable the feature: [Enable Encryption at Host](https://learn.microsoft.com/azure/virtual-machines/disks-enable-host-based-encryption-portal). +- **Enable AVD Private Link** this feature is not enabled on subscriptions by default. Use the following link to enable AVD Private Link on your subscription: [Enable the Feature](https://learn.microsoft.com/azure/virtual-desktop/private-link-setup?tabs=portal%2Cportal-2#enable-the-feature) + +### Optional + +- **Domain Services:** if you plan to domain or hybrid join the session hosts, ensure Active Directory Domain Services or Entra Domain Services is available in your enviroment and that you are synchronizing the required objects. AD Sites & Services should be configured for the address space of your Azure virtual network if you are extending your on premises Active Directory infrastruture into the cloud. +- **DNS:** if you plan to domain or hybrid join the sessions hosts, you must configure your DNS server in the MLZ Identity virtual network with ONE of the following options to support Private Link: + - DNS forwarder points to the [Azure VIP, 168.63.129.16](https://learn.microsoft.com/azure/virtual-network/what-is-ip-address-168-63-129-16). + - Conditional forwarders for the [Azure private DNS zones](https://learn.microsoft.com/azure/private-link/private-endpoint-dns) in the Hub resource group that point to the [Azure VIP, 168.63.129.16](https://learn.microsoft.com/azure/virtual-network/what-is-ip-address-168-63-129-16). +- **Domain Permissions** if using domain services, create a principal to domain join the session hosts and Azure Files, if applicable. + - Active Directory Domain Services: ensure the principal has the following permissions. + - "Join the Domain" on the domain + - "Create Computer" on the parent OU or domain + - "Delete Computer" on the parent OU or domain + - Entra Domain Services: ensure the principal is a member of the "AAD DC Administrators" group in Azure AD. +- **FSLogix with Azure NetApp Files:** the following steps must be completed if you plan to use this service. + - [Register the resource provider](https://learn.microsoft.com/azure/azure-netapp-files/azure-netapp-files-register) + - [Enable the shared AD feature](https://learn.microsoft.com/azure/azure-netapp-files/create-active-directory-connections#shared_ad) - this feature is required if you plan to deploy more than one domain joined NetApp account in the same Azure subscription and region. +- **Marketplace Image:** If you plan to deploy this solution using PowerShell or AzureCLI and use a marketplace image for the virtual machines, use the code below to find the appropriate image: + +```powershell +# Determine the Publisher; input the location for your AVD deployment +$Location = '' +(Get-AzVMImagePublisher -Location $Location).PublisherName + +# Determine the Offer; common publisher is 'MicrosoftWindowsDesktop' for Win 10/11 +$Publisher = '' +(Get-AzVMImageOffer -Location $Location -PublisherName $Publisher).Offer + +# Determine the SKU; common offers are 'Windows-10' for Win 10 and 'office-365' for the Win10/11 multi-session with M365 apps +$Offer = '' +(Get-AzVMImageSku -Location $Location -PublisherName $Publisher -Offer $Offer).Skus + +# Determine the Image Version; common offers are '21h1-evd-o365pp' and 'win11-21h2-avd-m365' +$Sku = '' +Get-AzVMImage -Location $Location -PublisherName $Publisher -Offer $Offer -Skus $Sku | Select-Object * | Format-List + +# Common version is 'latest' +``` diff --git a/src/bicep/add-ons/azureVirtualDesktop/docs/troubleshooting.md b/src/bicep/add-ons/azureVirtualDesktop/docs/troubleshooting.md new file mode 100644 index 000000000..0334c7993 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/docs/troubleshooting.md @@ -0,0 +1,7 @@ +# Azure Virtual Desktop Solution + +[**Home**](../README.md) | [**Features**](./features.md) | [**Design**](./design.md) | [**Prerequisites**](./prerequisites.md) | [**Troubleshooting**](./troubleshooting.md) + +## Troubleshooting + +If you need to redeploy this solution due to an error or to add resources, be sure the virtual machines (aka session hosts) are turned on. For "pooled" host pools, you must disable scaling as well. If the virtual machines are shutdown, the deployment will fail since virtual machine extensions cannot be updated when virtual machines are in a shutdown state. diff --git a/src/bicep/add-ons/azureVirtualDesktop/images/identifiers.png b/src/bicep/add-ons/azureVirtualDesktop/images/identifiers.png new file mode 100644 index 000000000..8d0209153 Binary files /dev/null and b/src/bicep/add-ons/azureVirtualDesktop/images/identifiers.png differ diff --git a/src/bicep/add-ons/azureVirtualDesktop/images/identifiers.vsdx b/src/bicep/add-ons/azureVirtualDesktop/images/identifiers.vsdx new file mode 100644 index 000000000..e3f923ac5 Binary files /dev/null and b/src/bicep/add-ons/azureVirtualDesktop/images/identifiers.vsdx differ diff --git a/src/bicep/add-ons/azureVirtualDesktop/images/resources.png b/src/bicep/add-ons/azureVirtualDesktop/images/resources.png new file mode 100644 index 000000000..94461e3a5 Binary files /dev/null and b/src/bicep/add-ons/azureVirtualDesktop/images/resources.png differ diff --git a/src/bicep/add-ons/azureVirtualDesktop/images/resources.vsdx b/src/bicep/add-ons/azureVirtualDesktop/images/resources.vsdx new file mode 100644 index 000000000..f0a0d9677 Binary files /dev/null and b/src/bicep/add-ons/azureVirtualDesktop/images/resources.vsdx differ diff --git a/src/bicep/add-ons/azureVirtualDesktop/images/stamps.png b/src/bicep/add-ons/azureVirtualDesktop/images/stamps.png new file mode 100644 index 000000000..fc8e58836 Binary files /dev/null and b/src/bicep/add-ons/azureVirtualDesktop/images/stamps.png differ diff --git a/src/bicep/add-ons/azureVirtualDesktop/images/stamps.vsdx b/src/bicep/add-ons/azureVirtualDesktop/images/stamps.vsdx new file mode 100644 index 000000000..f3615d226 Binary files /dev/null and b/src/bicep/add-ons/azureVirtualDesktop/images/stamps.vsdx differ diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/cleanUp/cleanUp.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/cleanUp/cleanUp.bicep new file mode 100644 index 000000000..3c25401ec --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/cleanUp/cleanUp.bicep @@ -0,0 +1,19 @@ +targetScope = 'subscription' + +param fslogixStorageService string +param location string +param resourceGroupManagement string +param scalingTool bool +param timestamp string +param userAssignedIdentityClientId string +param virtualMachineName string + +module removeManagementVirtualMachine 'removeVirtualMachine.bicep' = if (!scalingTool && !(fslogixStorageService == 'AzureFiles Premium')) { + scope: resourceGroup(resourceGroupManagement) + name: 'RemoveManagementVirtualMachine_${timestamp}' + params: { + Location: location + UserAssignedIdentityClientId: userAssignedIdentityClientId + VirtualMachineName: virtualMachineName + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/cleanUp/removeVirtualMachine.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/cleanUp/removeVirtualMachine.bicep new file mode 100644 index 000000000..421da1a5d --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/cleanUp/removeVirtualMachine.bicep @@ -0,0 +1,59 @@ +param Location string +param UserAssignedIdentityClientId string +param VirtualMachineName string + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2022-03-01' existing = { + name: VirtualMachineName +} + +resource removeVirtualMachine 'Microsoft.Compute/virtualMachines/runCommands@2023-03-01' = { + parent: virtualMachine + name: 'RunCommand' + location: Location + properties: { + treatFailureAsDeploymentFailure: true + asyncExecution: true + parameters: [ + { + name: 'Environment' + value: environment().name + } + { + name: 'ResourceGroupName' + value: resourceGroup().name + } + { + name: 'SubscriptionId' + value: subscription().subscriptionId + } + { + name: 'TenantId' + value: tenant().tenantId + } + { + name: 'UserAssignedIdentityClientId' + value: UserAssignedIdentityClientId + } + { + name: 'VirtualMachineName' + value: VirtualMachineName + } + + ] + source: { + script: ''' + param( + [string]$Environment, + [string]$ResourceGroupName, + [string]$SubscriptionId, + [string]$TenantId, + [string]$UserAssignedIdentityClientId, + [string]$VirtualMachineName + ) + Start-Sleep -Seconds 30 + Connect-AzAccount -Environment $Environment -Tenant $TenantId -Subscription $SubscriptionId -Identity -AccountId $UserAssignedIdentityClientId + Remove-AzVM -ResourceGroupName $ResourceGroupName -Name $VirtualMachineName -NoWait -Force + ''' + } + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/common/customScriptExtensions.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/common/customScriptExtensions.bicep new file mode 100644 index 000000000..e4a6414c5 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/common/customScriptExtensions.bicep @@ -0,0 +1,38 @@ +param fileUris array +param location string +@secure() +param parameters string +param scriptFileName string +param tags object +param timestamp string = utcNow('yyyyMMddhhmmss') +param userAssignedIdentityClientId string +param virtualMachineName string + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2023-03-01' existing = { + name: virtualMachineName +} + +resource customScriptExtension 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = { + parent: virtualMachine + name: 'CustomScriptExtension' + location: location + tags: tags + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + autoUpgradeMinorVersion: true + settings: { + timestamp: timestamp + } + protectedSettings: { + commandToExecute: 'powershell -ExecutionPolicy Unrestricted -File ${scriptFileName} ${parameters}' + fileUris: fileUris + managedIdentity: { + clientId: userAssignedIdentityClientId + } + } + } +} + +output value object = json(filter(customScriptExtension.properties.instanceView.substatuses, item => item.code == 'ComponentStatus/StdOut/succeeded')[0].message) diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/common/roleAssignment.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/common/roleAssignment.bicep new file mode 100644 index 000000000..47488388e --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/common/roleAssignment.bicep @@ -0,0 +1,12 @@ +param PrincipalId string +param PrincipalType string +param RoleDefinitionId string + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(PrincipalId, RoleDefinitionId, resourceGroup().id) + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', RoleDefinitionId) + principalId: PrincipalId + principalType: PrincipalType + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/applicationGroup.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/applicationGroup.bicep new file mode 100644 index 000000000..ecaa30b7f --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/applicationGroup.bicep @@ -0,0 +1,59 @@ +param artifactsUri string +param deploymentUserAssignedIdentityClientId string +param desktopApplicationGroupName string +param desktopFriendlyName string +param hostPoolResourceId string +param locationControlPlane string +param locationVirtualMachines string +param resourceGroupManagement string +param roleDefinitions object +param securityPrincipalObjectIds array +param tags object +param timestamp string +param virtualMachineName string + +resource applicationGroup 'Microsoft.DesktopVirtualization/applicationGroups@2021-03-09-preview' = { + name: desktopApplicationGroupName + location: locationControlPlane + tags: union({ + 'cm-resource-parent': hostPoolResourceId + }, contains(tags, 'Microsoft.DesktopVirtualization/applicationGroups') ? tags['Microsoft.DesktopVirtualization/applicationGroups'] : {}) + properties: { + hostPoolArmPath: hostPoolResourceId + applicationGroupType: 'Desktop' + } +} + +// Adds a friendly name to the SessionDesktop application for the desktop application group +module applicationFriendlyName '../common/customScriptExtensions.bicep' = if (!empty(desktopFriendlyName)) { + scope: resourceGroup(resourceGroupManagement) + name: 'ApplicationFriendlyName_${timestamp}' + params : { + fileUris: [ + '${artifactsUri}Update-AvdDesktop.ps1' + ] + location: locationVirtualMachines + parameters: '-ApplicationGroupName ${applicationGroup.name} -Environment ${environment().name} -FriendlyName "${desktopFriendlyName}" -ResourceGroupName ${resourceGroup().name} -SubscriptionId ${subscription().subscriptionId} -Tenant ${tenant().tenantId} -UserAssignedIdentityClientId ${deploymentUserAssignedIdentityClientId}' + scriptFileName: 'Update-AvdDesktop.ps1' + tags: union({ + 'cm-resource-parent': hostPoolResourceId + }, contains(tags, 'Microsoft.Compute/virtualMachines') ? tags['Microsoft.Compute/virtualMachines'] : {}) + userAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + virtualMachineName: virtualMachineName + } +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for i in range(0, length(securityPrincipalObjectIds)): { + scope: applicationGroup + name: guid(securityPrincipalObjectIds[i], roleDefinitions.DesktopVirtualizationUser, desktopApplicationGroupName) + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.DesktopVirtualizationUser) + principalId: securityPrincipalObjectIds[i] + } +}] + + +output applicationGroupReference array = [ + applicationGroup.id +] +output resourceId string = applicationGroup.id diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/controlPlane.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/controlPlane.bicep new file mode 100644 index 000000000..a3f6b2fb2 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/controlPlane.bicep @@ -0,0 +1,98 @@ +targetScope = 'subscription' + +param activeDirectorySolution string +param artifactsUri string +param avdPrivateDnsZoneResourceId string +param customRdpProperty string +param deploymentUserAssignedIdentityClientId string +param desktopApplicationGroupName string +param desktopFriendlyName string +param existingFeedWorkspace bool +param hostPoolName string +param hostPoolPublicNetworkAccess string +param hostPoolType string +param locationControlPlane string +param locationVirtualMachines string +param logAnalyticsWorkspaceResourceId string +param managementVirtualMachineName string +param maxSessionLimit int +param monitoring bool +param resourceGroupControlPlane string +param resourceGroupFeedWorkspace string +param resourceGroupManagement string +param roleDefinitions object +param securityPrincipalObjectIds array +param subnetResourceId string +param tags object +param timestamp string +param validationEnvironment bool +param vmTemplate string +param workspaceFriendlyName string +param workspaceNamePrefix string +param workspacePublicNetworkAccess string + +module hostPool 'hostPool.bicep' = { + name: 'HostPool_${timestamp}' + scope: resourceGroup(resourceGroupControlPlane) + params: { + activeDirectorySolution: activeDirectorySolution + avdPrivateDnsZoneResourceId: avdPrivateDnsZoneResourceId + customRdpProperty: customRdpProperty + hostPoolName: hostPoolName + hostPoolPublicNetworkAccess: hostPoolPublicNetworkAccess + hostPoolType: hostPoolType + location: locationControlPlane + logAnalyticsWorkspaceResourceId: logAnalyticsWorkspaceResourceId + maxSessionLimit: maxSessionLimit + monitoring: monitoring + subnetResourceId: subnetResourceId + tags: tags + validationEnvironment: validationEnvironment + vmTemplate: vmTemplate + } +} + +module applicationGroup 'applicationGroup.bicep' = { + name: 'ApplicationGroup_${timestamp}' + scope: resourceGroup(resourceGroupControlPlane) + params: { + artifactsUri: artifactsUri + deploymentUserAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + desktopApplicationGroupName: desktopApplicationGroupName + hostPoolResourceId: hostPool.outputs.ResourceId + locationControlPlane: locationControlPlane + locationVirtualMachines: locationVirtualMachines + resourceGroupManagement: resourceGroupManagement + roleDefinitions: roleDefinitions + securityPrincipalObjectIds: securityPrincipalObjectIds + desktopFriendlyName: desktopFriendlyName + tags: tags + timestamp: timestamp + virtualMachineName: managementVirtualMachineName + } +} + +module workspace 'workspace.bicep' = { + name: 'WorkspaceFeed_${timestamp}' + scope: resourceGroup(resourceGroupFeedWorkspace) + params: { + applicationGroupReferences: applicationGroup.outputs.applicationGroupReference + artifactsUri: artifactsUri + avdPrivateDnsZoneResourceId: avdPrivateDnsZoneResourceId + deploymentUserAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + existing: existingFeedWorkspace + friendlyName: workspaceFriendlyName + hostPoolName: hostPoolName + locationControlPlane: locationControlPlane + locationVirtualMachines: locationVirtualMachines + logAnalyticsWorkspaceResourceId: logAnalyticsWorkspaceResourceId + monitoring: monitoring + resourceGroupManagement: resourceGroupManagement + subnetResourceId: subnetResourceId + tags: tags + timestamp: timestamp + virtualMachineName: managementVirtualMachineName + workspaceNamePrefix: workspaceNamePrefix + workspacePublicNetworkAccess: workspacePublicNetworkAccess + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/hostPool.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/hostPool.bicep new file mode 100644 index 000000000..2bc579b89 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/hostPool.bicep @@ -0,0 +1,120 @@ +param activeDirectorySolution string +param avdPrivateDnsZoneResourceId string +param customRdpProperty string +param hostPoolName string +param hostPoolPublicNetworkAccess string +param hostPoolType string +param location string +param logAnalyticsWorkspaceResourceId string +param maxSessionLimit int +param monitoring bool +param subnetResourceId string +param tags object +param time string = utcNow('u') +param validationEnvironment bool +param vmTemplate string + +var customRdpProperty_Complete = contains(activeDirectorySolution, 'MicrosoftEntraId') ? '${customRdpProperty}targetisaadjoined:i:1;enablerdsaadauth:i:1;' : customRdpProperty +var hostPoolLogs = [ + { + category: 'Checkpoint' + enabled: true + } + { + category: 'Error' + enabled: true + } + { + category: 'Management' + enabled: true + } + { + category: 'Connection' + enabled: true + } + { + category: 'HostRegistration' + enabled: true + } + { + category: 'AgentHealthStatus' + enabled: true + } +] +var privateEndpointName = 'pe-${hostPoolName}' + +resource hostPool 'Microsoft.DesktopVirtualization/hostPools@2023-09-05' = { + name: hostPoolName + location: location + tags: union({ + 'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroup().name}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}' + }, contains(tags, 'Microsoft.DesktopVirtualization/hostPools') ? tags['Microsoft.DesktopVirtualization/hostPools'] : {}) + properties: { + customRdpProperty: customRdpProperty_Complete + hostPoolType: split(hostPoolType, ' ')[0] + loadBalancerType: contains(hostPoolType, 'Pooled') ? split(hostPoolType, ' ')[1] : 'Persistent' + maxSessionLimit: maxSessionLimit + personalDesktopAssignmentType: contains(hostPoolType, 'Personal') ? split(hostPoolType, ' ')[1] : null + preferredAppGroupType: 'Desktop' + publicNetworkAccess: hostPoolPublicNetworkAccess + registrationInfo: { + expirationTime: dateTimeAdd(time, 'PT2H') + registrationTokenOperation: 'Update' + } + startVMOnConnect: true + validationEnvironment: validationEnvironment + vmTemplate: vmTemplate + + } +} + +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = { + name: privateEndpointName + location: location + tags: union({ + 'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroup().name}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}' + }, contains(tags, 'Microsoft.Network/privateEndpoints') ? tags['Microsoft.Network/privateEndpoints'] : {}) + properties: { + customNetworkInterfaceName: 'nic-${hostPoolName}' + privateLinkServiceConnections: [ + { + name: privateEndpointName + properties: { + privateLinkServiceId: hostPool.id + groupIds: [ + 'connection' + ] + } + } + ] + subnet: { + id: subnetResourceId + } + } +} + +resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = { + parent: privateEndpoint + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: replace(split(avdPrivateDnsZoneResourceId, '/')[8], '.', '-') + properties: { + privateDnsZoneId: avdPrivateDnsZoneResourceId + } + } + ] + } +} + +resource hostPoolDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (monitoring) { + name: 'diag-${hostPoolName}' + scope: hostPool + properties: { + logs: hostPoolLogs + workspaceId: logAnalyticsWorkspaceResourceId + } +} + +output ResourceId string = hostPool.id diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/workspace.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/workspace.bicep new file mode 100644 index 000000000..b9d766c3a --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/controlPlane/workspace.bicep @@ -0,0 +1,114 @@ +param applicationGroupReferences array +param artifactsUri string +param avdPrivateDnsZoneResourceId string +param deploymentUserAssignedIdentityClientId string +param existing bool +param friendlyName string +param hostPoolName string +param locationControlPlane string +param locationVirtualMachines string +param logAnalyticsWorkspaceResourceId string +param monitoring bool +param resourceGroupManagement string +param subnetResourceId string +param tags object +param timestamp string +param virtualMachineName string +param workspaceNamePrefix string +param workspacePublicNetworkAccess string + +var feedWorkspaceName = '${workspaceNamePrefix}-feed' +var privateEndpointName = 'pe-${feedWorkspaceName}' + +module addApplicationGroups '../common/customScriptExtensions.bicep' = if (existing) { + scope: resourceGroup(resourceGroupManagement) + name: 'AddApplicationGroupReferences_${timestamp}' + params: { + fileUris: [ + '${artifactsUri}Update-AvdWorkspace.ps1' + ] + location: locationVirtualMachines + parameters: '-ApplicationGroupReferences "${applicationGroupReferences}" -Environment ${environment().name} -ResourceGroupName ${resourceGroup().name} -SubscriptionId ${subscription().subscriptionId} -TenantId ${tenant().tenantId} -UserAssignedIdentityClientId ${deploymentUserAssignedIdentityClientId} -WorkspaceName ${feedWorkspaceName}' + scriptFileName: 'Update-AvdWorkspace.ps1' + tags: union({ + 'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroup().name}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}' + }, contains(tags, 'Microsoft.Compute/virtualMachines') ? tags['Microsoft.Compute/virtualMachines'] : {}) + userAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + virtualMachineName: virtualMachineName + } +} + +resource workspace 'Microsoft.DesktopVirtualization/workspaces@2023-09-05' = if (!existing) { + name: feedWorkspaceName + location: locationControlPlane + tags: {} + properties: { + applicationGroupReferences: applicationGroupReferences + friendlyName: '${friendlyName} (${locationControlPlane})' + publicNetworkAccess: workspacePublicNetworkAccess + } +} + +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = if (!existing) { + name: privateEndpointName + location: locationControlPlane + tags: {} + properties: { + customNetworkInterfaceName: 'nic-${feedWorkspaceName}' + privateLinkServiceConnections: [ + { + name: privateEndpointName + properties: { + privateLinkServiceId: workspace.id + groupIds: [ + 'feed' + ] + } + } + ] + subnet: { + id: subnetResourceId + } + } +} + +resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = if (!existing) { + parent: privateEndpoint + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: replace(split(avdPrivateDnsZoneResourceId, '/')[8], '.', '-') + properties: { + privateDnsZoneId: avdPrivateDnsZoneResourceId + } + } + ] + } +} + +resource workspaceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!existing && monitoring) { + name: 'diag-${feedWorkspaceName}' + scope: workspace + properties: { + logs: [ + { + category: 'Checkpoint' + enabled: true + } + { + category: 'Error' + enabled: true + } + { + category: 'Management' + enabled: true + } + { + category: 'Feed' + enabled: true + } + ] + workspaceId: logAnalyticsWorkspaceResourceId + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/azureFiles.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/azureFiles.bicep new file mode 100644 index 000000000..4b8a33a2b --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/azureFiles.bicep @@ -0,0 +1,270 @@ +param artifactsUri string +param automationAccountName string +param availability string +param azureFilesPrivateDnsZoneResourceId string +param deploymentUserAssignedIdentityClientId string +@secure() +param domainJoinPassword string +param domainJoinUserPrincipalName string +param enableRecoveryServices bool +param encryptionUserAssignedIdentityResourceId string +param activeDirectorySolution string +param fileShares array +param fslogixShareSizeInGB int +param fslogixContainerType string +param fslogixStorageService string +param hostPoolType string +param keyVaultUri string +param location string +param managementVirtualMachineName string +param netbios string +param organizationalUnitPath string +param recoveryServicesVaultName string +param resourceGroupManagement string +param resourceGroupStorage string +param securityPrincipalObjectIds array +param securityPrincipalNames array +@minLength(3) +param storageAccountNamePrefix string +param storageCount int +param storageEncryptionKeyName string +param storageIndex int +param storageSku string +param storageService string +param subnet string +param tagsAutomationAccounts object +param tagsPrivateEndpoints object +param tagsRecoveryServicesVault object +param tagsStorageAccounts object +param tagsVirtualMachines object +param timestamp string +param timeZone string +param virtualNetwork string +param virtualNetworkResourceGroup string + +var roleDefinitionId = '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb' // Storage File Data SMB Share Contributor +var smbMultiChannel = { + multichannel: { + enabled: true + } +} +var smbSettings = { + versions: 'SMB3.1.1;' + authenticationMethods: 'NTLMv2;Kerberos;' + kerberosTicketEncryption: 'AES-256;' + channelEncryption: 'AES-128-GCM;AES-256-GCM;' +} +var storageRedundancy = availability == 'availabilityZones' ? '_ZRS' : '_LRS' +var subnetId = resourceId(virtualNetworkResourceGroup, 'Microsoft.Network/virtualNetworks/subnets', virtualNetwork, subnet) + +resource storageAccounts 'Microsoft.Storage/storageAccounts@2022-09-01' = [for i in range(0, storageCount): { + name: '${storageAccountNamePrefix}${padLeft(i + storageIndex, 2, '0')}' + location: location + tags: tagsStorageAccounts + sku: { + name: '${storageSku}${storageRedundancy}' + } + kind: storageSku == 'Standard' ? 'StorageV2' : 'FileStorage' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${encryptionUserAssignedIdentityResourceId}': {} + } + } + properties: { + accessTier: 'Hot' + allowBlobPublicAccess: false + allowCrossTenantReplication: false + allowedCopyScope: 'PrivateLink' + allowSharedKeyAccess: true + azureFilesIdentityBasedAuthentication: { + directoryServiceOptions: activeDirectorySolution == 'MicrosoftEntraDomainServices' ? 'AADDS' : 'None' + } + defaultToOAuthAuthentication: false + dnsEndpointType: 'Standard' + encryption: { + identity: { + userAssignedIdentity: encryptionUserAssignedIdentityResourceId + } + requireInfrastructureEncryption: true + keyvaultproperties: { + keyvaulturi: keyVaultUri + keyname: storageEncryptionKeyName + } + services: storageSku == 'Standard' ? { + file: { + keyType: 'Account' + enabled: true + } + table: { + keyType: 'Account' + enabled: true + } + queue: { + keyType: 'Account' + enabled: true + } + blob: { + keyType: 'Account' + enabled: true + } + } : { + file: { + keyType: 'Account' + enabled: true + } + } + keySource: 'Microsoft.KeyVault' + } + largeFileSharesState: storageSku == 'Standard' ? 'Enabled' : null + minimumTlsVersion: 'TLS1_2' + networkAcls: { + bypass: 'AzureServices' + virtualNetworkRules: [] + ipRules: [] + defaultAction: 'Deny' + } + publicNetworkAccess: 'Disabled' + supportsHttpsTrafficOnly: true + } +}] + +// Assigns the SMB Contributor role to the Storage Account so users can save their profiles to the file share using FSLogix +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for i in range(0, storageCount): { + scope: storageAccounts[i] + name: guid(securityPrincipalObjectIds[i], roleDefinitionId, storageAccounts[i].id) + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) + principalId: securityPrincipalObjectIds[i] + } +}] + +resource fileServices 'Microsoft.Storage/storageAccounts/fileServices@2022-09-01' = [for i in range(0, storageCount): { + parent: storageAccounts[i] + name: 'default' + properties: { + protocolSettings: { + smb: storageSku == 'Standard' ? smbSettings : union(smbSettings, smbMultiChannel) + } + shareDeleteRetentionPolicy: { + enabled: false + } + } +}] + +module shares 'shares.bicep' = [for i in range(0, storageCount): { + name: 'fileShares_${i}_${timestamp}' + params: { + fileShares: fileShares + fslogixShareSizeInGB: fslogixShareSizeInGB + storageAccountName: storageAccounts[i].name + storageSku: storageSku + } + dependsOn: [ + roleAssignment + ] +}] + +resource privateEndpoints 'Microsoft.Network/privateEndpoints@2023-04-01' = [for i in range(0, storageCount): { + name: 'pe-${storageAccountNamePrefix}${padLeft(i + storageIndex, 2, '0')}-file' + location: location + tags: tagsPrivateEndpoints + properties: { + customNetworkInterfaceName: 'nic-${storageAccountNamePrefix}${padLeft(i + storageIndex, 2, '0')}-file' + privateLinkServiceConnections: [ + { + name: 'pe-${storageAccounts[i].name}' + properties: { + privateLinkServiceId: storageAccounts[i].id + groupIds: [ + 'file' + ] + } + } + ] + subnet: { + id: subnetId + } + } +}] + +resource privateDnsZoneGroups 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-08-01' = [for i in range(0, storageCount): { + parent: privateEndpoints[i] + name: '${storageAccountNamePrefix}${padLeft(i + storageIndex, 2, '0')}' + properties: { + privateDnsZoneConfigs: [ + { + name: 'ipconfig1' + properties: { + privateDnsZoneId: azureFilesPrivateDnsZoneResourceId + } + } + ] + } + dependsOn: [ + storageAccounts + ] +}] + +module ntfsPermissions '../../common/customScriptExtensions.bicep' = if (contains(activeDirectorySolution, 'DomainServices')) { + name: 'FslogixNtfsPermissions_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + fileUris: [ + '${artifactsUri}Set-NtfsPermissions.ps1' + ] + location: location + parameters: '-domainJoinPassword "${domainJoinPassword}" -domainJoinUserPrincipalName ${domainJoinUserPrincipalName} -activeDirectorySolution ${activeDirectorySolution} -Environment ${environment().name} -fslogixContainerType ${fslogixContainerType} -netbios ${netbios} -organizationalUnitPath "${organizationalUnitPath}" -securityPrincipalNames "${securityPrincipalNames}" -StorageAccountPrefix ${storageAccountNamePrefix} -StorageAccountResourceGroupName ${resourceGroupStorage} -storageCount ${storageCount} -storageIndex ${storageIndex} -storageService ${storageService} -StorageSuffix ${environment().suffixes.storage} -SubscriptionId ${subscription().subscriptionId} -TenantId ${subscription().tenantId} -UserAssignedIdentityClientId ${deploymentUserAssignedIdentityClientId}' + scriptFileName: 'Set-NtfsPermissions.ps1' + tags: tagsVirtualMachines + userAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + virtualMachineName: managementVirtualMachineName + } + dependsOn: [ + privateDnsZoneGroups + privateEndpoints + shares + ] +} + +module recoveryServices 'recoveryServices.bicep' = if (enableRecoveryServices && contains(hostPoolType, 'Pooled')) { + name: 'recoveryServices_AzureFiles_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + fileShares: fileShares + location: location + recoveryServicesVaultName: recoveryServicesVaultName + resourceGroupStorage: resourceGroupStorage + storageAccountNamePrefix: storageAccountNamePrefix + storageCount: storageCount + storageIndex: storageIndex + tagsRecoveryServicesVault: tagsRecoveryServicesVault + timestamp: timestamp + } + dependsOn: [ + shares + ] +} + +module autoIncreasePremiumFileShareQuota '../../management/autoIncreasePremiumFileShareQuota.bicep' = if (fslogixStorageService == 'AzureFiles Premium' && storageCount > 0) { + name: 'AutoIncreasePremiumFileShareQuota_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + artifactsUri: artifactsUri + automationAccountName: automationAccountName + deploymentUserAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + fslogixContainerType: fslogixContainerType + location: location + managementVirtualMachineName: managementVirtualMachineName + storageAccountNamePrefix: storageAccountNamePrefix + storageCount: storageCount + storageIndex: storageIndex + storageResourceGroupName: resourceGroupStorage + tags: tagsAutomationAccounts + timestamp: timestamp + timeZone: timeZone + } + dependsOn: [ + ntfsPermissions + ] +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/protectedItems.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/protectedItems.bicep new file mode 100644 index 000000000..13770d4e5 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/protectedItems.bicep @@ -0,0 +1,19 @@ +param fileShares array +param location string +param policyId string +param protectionContainerName string +param sourceResourceId string +param tags object + +// Only configures backups for profile containers +// Office containers contain M365 cached data that does not need to be backed up +resource protectedItems_FileShare 'Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems@2022-03-01' = [for fileShare in fileShares: if (contains(fileShare, 'profile')) { + name: '${protectionContainerName}/AzureFileShare;${fileShare}' + location: location + tags: tags + properties: { + protectedItemType: 'AzureFileShareProtectedItem' + policyId: policyId + sourceResourceId: sourceResourceId + } +}] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/recoveryServices.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/recoveryServices.bicep new file mode 100644 index 000000000..75a6f7faf --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/recoveryServices.bicep @@ -0,0 +1,39 @@ +param fileShares array +param location string +param recoveryServicesVaultName string +param resourceGroupStorage string +param storageAccountNamePrefix string +param storageCount int +param storageIndex int +param tagsRecoveryServicesVault object +param timestamp string + +resource vault 'Microsoft.RecoveryServices/vaults@2022-03-01' existing = { + name: recoveryServicesVaultName +} + +resource protectionContainers 'Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers@2022-03-01' = [for i in range(0, storageCount): { + name: '${vault.name}/Azure/storagecontainer;Storage;${resourceGroupStorage};${storageAccountNamePrefix}${padLeft(i + storageIndex, 2, '0')}' + properties: { + backupManagementType: 'AzureStorage' + containerType: 'StorageContainer' + sourceResourceId: resourceId(resourceGroupStorage, 'Microsoft.Storage/storageAccounts', '${storageAccountNamePrefix}${padLeft(i + storageIndex, 2, '0')}') + } +}] + +resource backupPolicy_Storage 'Microsoft.RecoveryServices/vaults/backupPolicies@2022-03-01' existing = { + parent: vault + name: 'AvdPolicyStorage' +} + +module protectedItems_fileShares 'protectedItems.bicep' = [for i in range(0, storageCount): { + name: 'BackupProtectedItems_fileShares_${i + storageIndex}_${timestamp}' + params: { + fileShares: fileShares + location: location + protectionContainerName: protectionContainers[i].name + policyId: backupPolicy_Storage.id + sourceResourceId: resourceId(resourceGroupStorage, 'Microsoft.Storage/storageAccounts', '${storageAccountNamePrefix}${padLeft(i + storageIndex, 2, '0')}') + tags: tagsRecoveryServicesVault + } +}] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/shares.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/shares.bicep new file mode 100644 index 000000000..ec2a84601 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureFiles/shares.bicep @@ -0,0 +1,13 @@ +param fileShares array +param fslogixShareSizeInGB int +param storageAccountName string +param storageSku string + +resource shares 'Microsoft.Storage/storageAccounts/fileServices/shares@2022-09-01' = [for i in range(0, length(fileShares)): { + name: '${storageAccountName}/default/${fileShares[i]}' + properties: { + accessTier: storageSku == 'Premium' ? 'Premium' : 'TransactionOptimized' + shareQuota: fslogixShareSizeInGB + enabledProtocols: 'SMB' + } +}] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureNetAppFiles.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureNetAppFiles.bicep new file mode 100644 index 000000000..a1fc52562 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/azureNetAppFiles.bicep @@ -0,0 +1,157 @@ +param artifactsUri string +param activeDirectoryConnection string +param delegatedSubnetId string +param deploymentUserAssignedIdentityClientId string +param dnsServers string +@secure() +param domainJoinPassword string +param domainJoinUserPrincipalName string +param domainName string +param fileShares array +param fslogixContainerType string +param location string +param managementVirtualMachineName string +param netAppAccountName string +param netAppCapacityPoolName string +param organizationalUnitPath string +param resourceGroupManagement string +param securityPrincipalNames array +param smbServerLocation string +param storageSku string +param storageService string +param tagsNetAppAccount object +param tagsVirtualMachines object +param timestamp string + +resource netAppAccount 'Microsoft.NetApp/netAppAccounts@2021-06-01' = { + name: netAppAccountName + location: location + tags: tagsNetAppAccount + properties: { + activeDirectories: activeDirectoryConnection == 'false' ? null : [ + { + aesEncryption: true + domain: domainName + dns: dnsServers + organizationalUnit: organizationalUnitPath + password: domainJoinPassword + smbServerName: 'anf-${smbServerLocation}' + username: split(domainJoinUserPrincipalName, '@')[0] + } + ] + encryption: { + keySource: 'Microsoft.NetApp' + } + } +} + +resource capacityPool 'Microsoft.NetApp/netAppAccounts/capacityPools@2021-06-01' = { + parent: netAppAccount + name: netAppCapacityPoolName + location: location + tags: tagsNetAppAccount + properties: { + coolAccess: false + encryptionType: 'Single' + qosType: 'Auto' + serviceLevel: storageSku + size: 4398046511104 + } +} + +resource volumes 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes@2021-06-01' = [for i in range(0, length(fileShares)): { + parent: capacityPool + name: fileShares[i] + location: location + tags: tagsNetAppAccount + properties: { + avsDataStore: 'Disabled' + // backupId: 'string' + coolAccess: false + // coolnessPeriod: int + creationToken: fileShares[i] + // dataProtection: { + // backup: { + // backupEnabled: bool + // backupPolicyId: 'string' + // policyEnforced: bool + // vaultId: 'string' + // } + // replication: { + // endpointType: 'string' + // remoteVolumeRegion: 'string' + // remoteVolumeResourceId: 'string' + // replicationId: 'string' + // replicationSchedule: 'string' + // } + // snapshot: { + // snapshotPolicyId: 'string' + // } + // } + defaultGroupQuotaInKiBs: 0 + defaultUserQuotaInKiBs: 0 + encryptionKeySource: 'Microsoft.NetApp' + // exportPolicy: { + // rules: [ + // { + // allowedClients: 'string' + // chownMode: 'string' + // cifs: bool + // hasRootAccess: bool + // kerberos5iReadWrite: bool + // kerberos5pReadWrite: bool + // kerberos5ReadWrite: bool + // nfsv3: bool + // nfsv41: bool + // ruleIndex: int + // unixReadWrite: bool + // } + // ] + // } + isDefaultQuotaEnabled: false + // isRestoring: bool + kerberosEnabled: false + ldapEnabled: false + networkFeatures: 'Standard' + protocolTypes: [ + 'CIFS' + ] + securityStyle: 'ntfs' + serviceLevel: storageSku + smbContinuouslyAvailable: true + smbEncryption: true + snapshotDirectoryVisible: true + // snapshotId: 'string' + subnetId: delegatedSubnetId + // throughputMibps: int + // unixPermissions: 'string' + usageThreshold: 107374182400 + // volumeType: 'string' + } +}] + +module ntfsPermissions '../common/customScriptExtensions.bicep' = { + name: 'FslogixNtfsPermissions_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + fileUris: [ + '${artifactsUri}Set-NtfsPermissions.ps1' + ] + location: location + parameters: '-domainJoinPassword "${domainJoinPassword}" -domainJoinUserPrincipalName ${domainJoinUserPrincipalName} -fslogixContainerType ${fslogixContainerType} -securityPrincipalNames "${securityPrincipalNames}" -smbServerLocation ${smbServerLocation} -storageService ${storageService}' + scriptFileName: 'Set-NtfsPermissions.ps1' + tags: tagsVirtualMachines + userAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + virtualMachineName: managementVirtualMachineName + } + dependsOn: [ + volumes + ] +} + +output fileShares array = contains(fslogixContainerType, 'Office') ? [ + volumes[0].properties.mountTargets[0].smbServerFqdn + volumes[1].properties.mountTargets[0].smbServerFqdn +] : [ + volumes[0].properties.mountTargets[0].smbServerFqdn +] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/fslogix.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/fslogix.bicep new file mode 100644 index 000000000..09a07118e --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/fslogix/fslogix.bicep @@ -0,0 +1,140 @@ +targetScope = 'subscription' + +param artifactsUri string +param activeDirectoryConnection string +param activeDirectorySolution string +param automationAccountName string +param availability string +param azureFilesPrivateDnsZoneResourceId string +param delegatedSubnetId string +param deploymentUserAssignedIdentityClientId string +param dnsServers string +@secure() +param domainJoinPassword string +param domainJoinUserPrincipalName string +param domainName string +param encryptionUserAssignedIdentityResourceId string +param fileShares array +param fslogixShareSizeInGB int +param fslogixContainerType string +param fslogixStorageService string +param hostPoolName string +param hostPoolType string +param keyVaultUri string +param location string +param managementVirtualMachineName string +param netAppAccountName string +param netAppCapacityPoolName string +param netbios string +param organizationalUnitPath string +param recoveryServices bool +param recoveryServicesVaultName string +param resourceGroupControlPlane string +param resourceGroupManagement string +param resourceGroupStorage string +param securityPrincipalObjectIds array +param securityPrincipalNames array +param smbServerLocation string +param storageAccountNamePrefix string +param storageCount int +param storageEncryptionKeyName string +param storageIndex int +param storageSku string +param storageService string +param subnet string +param tags object +param timestamp string +param timeZone string +param virtualNetwork string +param virtualNetworkResourceGroup string + +var tagsAutomationAccounts = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupControlPlane}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.Automation/automationAccounts') ? tags['Microsoft.Automation/automationAccounts'] : {}) +var tagsNetAppAccount = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupControlPlane}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.NetApp/netAppAccounts') ? tags['Microsoft.NetApp/netAppAccounts'] : {}) +var tagsPrivateEndpoints = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupControlPlane}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.Network/privateEndpoints') ? tags['Microsoft.Network/privateEndpoints'] : {}) +var tagsStorageAccounts = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupControlPlane}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.Storage/storageAccounts') ? tags['Microsoft.Storage/storageAccounts'] : {}) +var tagsRecoveryServicesVault = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupControlPlane}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.recoveryServices/vaults') ? tags['Microsoft.recoveryServices/vaults'] : {}) +var tagsVirtualMachines = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupControlPlane}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.Compute/virtualMachines') ? tags['Microsoft.Compute/virtualMachines'] : {}) + +// Azure NetApp Files for Fslogix +module azureNetAppFiles 'azureNetAppFiles.bicep' = if (storageService == 'AzureNetAppFiles' && contains(activeDirectorySolution, 'DomainServices')) { + name: 'AzureNetAppFiles_${timestamp}' + scope: resourceGroup(resourceGroupStorage) + params: { + artifactsUri: artifactsUri + activeDirectoryConnection: activeDirectoryConnection + delegatedSubnetId: delegatedSubnetId + dnsServers: dnsServers + domainJoinPassword: domainJoinPassword + domainJoinUserPrincipalName: domainJoinUserPrincipalName + domainName: domainName + fileShares: fileShares + fslogixContainerType: fslogixContainerType + location: location + managementVirtualMachineName: managementVirtualMachineName + netAppAccountName: netAppAccountName + netAppCapacityPoolName: netAppCapacityPoolName + organizationalUnitPath: organizationalUnitPath + resourceGroupManagement: resourceGroupManagement + securityPrincipalNames: securityPrincipalNames + smbServerLocation: smbServerLocation + storageSku: storageSku + storageService: storageService + tagsNetAppAccount: tagsNetAppAccount + tagsVirtualMachines: tagsVirtualMachines + timestamp: timestamp + deploymentUserAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + } +} + +// Azure Files for FSLogix +module azureFiles 'azureFiles/azureFiles.bicep' = if (storageService == 'AzureFiles' && contains(activeDirectorySolution, 'DomainServices')) { + name: 'AzureFiles_${timestamp}' + scope: resourceGroup(resourceGroupStorage) + params: { + activeDirectorySolution: activeDirectorySolution + artifactsUri: artifactsUri + automationAccountName: automationAccountName + availability: availability + azureFilesPrivateDnsZoneResourceId: azureFilesPrivateDnsZoneResourceId + deploymentUserAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + domainJoinPassword: domainJoinPassword + domainJoinUserPrincipalName: domainJoinUserPrincipalName + enableRecoveryServices: recoveryServices + encryptionUserAssignedIdentityResourceId: encryptionUserAssignedIdentityResourceId + fileShares: fileShares + fslogixContainerType: fslogixContainerType + fslogixShareSizeInGB: fslogixShareSizeInGB + fslogixStorageService: fslogixStorageService + hostPoolType: hostPoolType + keyVaultUri: keyVaultUri + location: location + managementVirtualMachineName: managementVirtualMachineName + netbios: netbios + organizationalUnitPath: organizationalUnitPath + recoveryServicesVaultName: recoveryServicesVaultName + resourceGroupManagement: resourceGroupManagement + resourceGroupStorage: resourceGroupStorage + securityPrincipalNames: securityPrincipalNames + securityPrincipalObjectIds: securityPrincipalObjectIds + storageAccountNamePrefix: storageAccountNamePrefix + storageCount: storageCount + storageEncryptionKeyName: storageEncryptionKeyName + storageIndex: storageIndex + storageService: storageService + storageSku: storageSku + subnet: subnet + tagsAutomationAccounts: tagsAutomationAccounts + tagsPrivateEndpoints: tagsPrivateEndpoints + tagsRecoveryServicesVault: tagsRecoveryServicesVault + tagsStorageAccounts: tagsStorageAccounts + tagsVirtualMachines: tagsVirtualMachines + timestamp: timestamp + timeZone: timeZone + virtualNetwork: virtualNetwork + virtualNetworkResourceGroup: virtualNetworkResourceGroup + } +} + +output netAppShares array = storageService == 'AzureNetAppFiles' ? azureNetAppFiles.outputs.fileShares : [ + 'None' +] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/hub/hub.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/hub/hub.bicep new file mode 100644 index 000000000..18428d791 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/hub/hub.bicep @@ -0,0 +1,41 @@ +targetScope = 'subscription' + +param existingWorkspace bool +param globalWorkspacePrivateDnsZoneResourceId string +param hubSubnetResourceId string +param resourceGroupName string +param timestamp string +param workspaceNamePrefix string + +module virtualNetwork 'virtualNetwork.bicep' = if (!existingWorkspace) { + scope: resourceGroup(split(hubSubnetResourceId, '/')[4]) + name: 'SharedServices_VirtualNetwork_${timestamp}' + params: { + name: split(hubSubnetResourceId, '/')[8] + } +} + +// Resource Group for the global AVD Workspace +module rg_GlobalWorkspace '../resourceGroup.bicep' = if (!existingWorkspace) { + name: 'ResourceGroup_WorkspaceGlobal_${timestamp}' + scope: subscription(split(hubSubnetResourceId, '/')[2]) + params: { + location: !existingWorkspace ? virtualNetwork.outputs.location : '' + resourceGroupName: resourceGroupName + tags: {} + } +} + +module workspace 'workspace.bicep' = if (!existingWorkspace) { + name: 'WorkspaceGlobal_${timestamp}' + scope: resourceGroup(resourceGroupName) + params: { + globalWorkspacePrivateDnsZoneResourceId: globalWorkspacePrivateDnsZoneResourceId + location: !existingWorkspace ? virtualNetwork.outputs.location : '' + subnetResourceId: hubSubnetResourceId + workspaceNamePrefix: workspaceNamePrefix + } + dependsOn: [ + rg_GlobalWorkspace + ] +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/hub/virtualNetwork.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/hub/virtualNetwork.bicep new file mode 100644 index 000000000..252f12058 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/hub/virtualNetwork.bicep @@ -0,0 +1,7 @@ +param name string + +resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-06-01' existing = { + name: name +} + +output location string = virtualNetwork.location diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/hub/workspace.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/hub/workspace.bicep new file mode 100644 index 000000000..7c9a762d1 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/hub/workspace.bicep @@ -0,0 +1,52 @@ +param globalWorkspacePrivateDnsZoneResourceId string +param location string +param subnetResourceId string +param workspaceNamePrefix string + +var globalWorkspaceName = '${workspaceNamePrefix}-global' +var privateEndpointName = 'pe-${globalWorkspaceName}' + +resource workspace 'Microsoft.DesktopVirtualization/workspaces@2023-09-05' = { + name: globalWorkspaceName + location: location + tags: {} + properties: {} +} + +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = { + name: privateEndpointName + location: location + tags: {} + properties: { + customNetworkInterfaceName: 'nic-${globalWorkspaceName}' + privateLinkServiceConnections: [ + { + name: privateEndpointName + properties: { + privateLinkServiceId: workspace.id + groupIds: [ + 'global' + ] + } + } + ] + subnet: { + id: subnetResourceId + } + } +} + +resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = { + parent: privateEndpoint + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: replace(split(globalWorkspacePrivateDnsZoneResourceId, '/')[8], '.', '-') + properties: { + privateDnsZoneId: globalWorkspacePrivateDnsZoneResourceId + } + } + ] + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/logic.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/logic.bicep new file mode 100644 index 000000000..6c483a49e --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/logic.bicep @@ -0,0 +1,93 @@ +targetScope = 'subscription' + +param activeDirectorySolution string +param deploymentLocations array +param diskSku string +param domainName string +param fileShareNames object +param fslogixContainerType string +param fslogixStorageService string +param hostPoolType string +param imageOffer string +param imagePublisher string +param imageSku string +param locations object +param locationVirtualMachines string +param resourceGroupControlPlane string +param resourceGroupFeedWorkspace string +param resourceGroupHosts string +param resourceGroupManagement string +param resourceGroupsNetwork array +param resourceGroupStorage string +param securityPrincipals array +param sessionHostCount int +param sessionHostIndex int +param virtualMachineNamePrefix string +param virtualMachineSize string + +// BATCH SESSION HOSTS +// The following variables are used to determine the batches to deploy any number of AVD session hosts. +var maxResourcesPerTemplateDeployment = 88 // This is the max number of session hosts that can be deployed from the sessionHosts.bicep file in each batch / for loop. Math: (800 - ) / +var divisionValue = sessionHostCount / maxResourcesPerTemplateDeployment // This determines if any full batches are required. +var divisionRemainderValue = sessionHostCount % maxResourcesPerTemplateDeployment // This determines if any partial batches are required. +var sessionHostBatchCount = divisionRemainderValue > 0 ? divisionValue + 1 : divisionValue // This determines the total number of batches needed, whether full and / or partial. + +// BATCH AVAILABILITY SETS +// The following variables are used to determine the number of availability sets. +var maxAvSetMembers = 200 // This is the max number of session hosts that can be deployed in an availability set. +var beginAvSetRange = sessionHostIndex / maxAvSetMembers // This determines the availability set to start with. +var endAvSetRange = (sessionHostCount + sessionHostIndex) / maxAvSetMembers // This determines the availability set to end with. +var availabilitySetsCount = length(range(beginAvSetRange, (endAvSetRange - beginAvSetRange) + 1)) + +// OTHER LOGIC & COMPUTED VALUES +var fileShares = fileShareNames[fslogixContainerType] +var fslogix = fslogixStorageService == 'None' || !contains(activeDirectorySolution, 'DomainServices') ? false : true +var netbios = split(domainName, '.')[0] +var pooledHostPool = split(hostPoolType, ' ')[0] == 'Pooled' ? true : false +var resourceGroups = union(resourceGroupsCommon, resourceGroupsNetworking, resourceGroupsStorage) +var resourceGroupsCommon = [ + resourceGroupControlPlane + resourceGroupFeedWorkspace + resourceGroupHosts + resourceGroupManagement +] +var resourceGroupsNetworking = length(deploymentLocations) == 2 ? resourceGroupsNetwork : [ + resourceGroupsNetwork[0] +] +var resourceGroupsStorage = fslogix ? [ + resourceGroupStorage +] : [] +var roleDefinitions = { + DesktopVirtualizationPowerOnContributor: '489581de-a3bd-480d-9518-53dea7416b33' + DesktopVirtualizationUser: '1d18fff3-a72a-46b5-b4a9-0b38a3cd7e63' + Reader: 'acdd72a7-3385-48ef-bd42-f606fba81ae7' + VirtualMachineUserLogin: 'fb879df8-f326-4884-b1cf-06f3ad86be52' +} +var securityPrincipalsCount = length(securityPrincipals) +var smbServerLocation = locations[locationVirtualMachines].abbreviation +var storageSku = fslogixStorageService == 'None' ? 'None' : split(fslogixStorageService, ' ')[1] +var storageService = split(fslogixStorageService, ' ')[0] +var storageSuffix = environment().suffixes.storage +var timeDifference = locations[locationVirtualMachines].timeDifference +var timeZone = locations[locationVirtualMachines].timeZone +var vmTemplate = '{"domain":"${domainName}","galleryImageOffer":"${imageOffer}","galleryImagePublisher":"${imagePublisher}","galleryImageSKU":"${imageSku}","imageType":"Gallery","imageUri":null,"customImageId":null,"namePrefix":"${virtualMachineNamePrefix}","osDiskType":"${diskSku}","useManagedDisks":true,"VirtualMachineSize":{"id":"${virtualMachineSize}","cores":null,"ram":null},"galleryItemId":"${imagePublisher}.${imageOffer}${imageSku}"}' + +output availabilitySetsCount int = availabilitySetsCount +output beginAvSetRange int = beginAvSetRange +output divisionRemainderValue int = divisionRemainderValue +output fileShares array = fileShares +output fslogix bool = fslogix +output maxResourcesPerTemplateDeployment int = maxResourcesPerTemplateDeployment +output netbios string = netbios +output pooledHostPool bool = pooledHostPool +output resourceGroups array = resourceGroups +output roleDefinitions object = roleDefinitions +output sessionHostBatchCount int = sessionHostBatchCount +output securityPrincipalsCount int = securityPrincipalsCount +output smbServerLocation string = smbServerLocation +output storageSku string = storageSku +output storageService string = storageService +output storageSuffix string = storageSuffix +output timeDifference string = timeDifference +output timeZone string = timeZone +output vmTemplate string = vmTemplate diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/artifacts.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/artifacts.bicep new file mode 100644 index 000000000..653f7293a --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/artifacts.bicep @@ -0,0 +1,38 @@ +param location string +param resourceGroupManagement string +param storageAccountName string +param subscriptionId string +param tags object +param timestamp string +param userAssignedIdentityNamePrefix string + +var name = '${userAssignedIdentityNamePrefix}-artifacts' +var roleDefinitionId = '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' // Storage Blob Data Reader + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = { + name: storageAccountName +} + +module userAssignedIdentity 'userAssignedIdentity.bicep' = { + scope: resourceGroup(subscriptionId, resourceGroupManagement) + name: 'UAI_Artifacts_${timestamp}' + params: { + location: location + name: name + tags: contains(tags, 'Microsoft.ManagedIdentity/userAssignedIdentities') ? tags['Microsoft.ManagedIdentity/userAssignedIdentities'] : {} + } +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storageAccount + name: guid(name, roleDefinitionId, resourceGroup().id) + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + } +} + +output userAssignedIdentityClientId string = userAssignedIdentity.outputs.clientId +output userAssignedIdentityPrincipalId string = userAssignedIdentity.outputs.principalId +output userAssignedIdentityResourceId string = userAssignedIdentity.outputs.resourceId diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/autoIncreasePremiumFileShareQuota.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/autoIncreasePremiumFileShareQuota.bicep new file mode 100644 index 000000000..a1e576adc --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/autoIncreasePremiumFileShareQuota.bicep @@ -0,0 +1,75 @@ +param artifactsUri string +param automationAccountName string +param deploymentUserAssignedIdentityClientId string +param fslogixContainerType string +param location string +param managementVirtualMachineName string +param storageAccountNamePrefix string +param storageCount int +param storageIndex int +param storageResourceGroupName string +param tags object +param timestamp string +param timeZone string + +var runbookFileName = 'Set-FileShareScaling.ps1' +var scriptFileName = 'Set-AutomationRunbook.ps1' +var subscriptionId = subscription().subscriptionId + +resource automationAccount 'Microsoft.Automation/automationAccounts@2022-08-08' existing = { + name: automationAccountName +} + +module runbook '../common/customScriptExtensions.bicep' = { + name: 'Runbook_QuotaScaling_${timestamp}' + params: { + fileUris: [ + '${artifactsUri}${runbookFileName}' + '${artifactsUri}${scriptFileName}' + ] + location: location + parameters: '-AutomationAccountName ${automationAccountName} -Environment ${environment().name} -ResourceGroupName ${resourceGroup().name} -RunbookFileName ${runbookFileName} -SubscriptionId ${subscription().subscriptionId} -TenantId ${tenant().tenantId} -UserAssignedIdentityClientId ${deploymentUserAssignedIdentityClientId}' + scriptFileName: scriptFileName + tags: contains(tags, 'Microsoft.Compute/virtualMachines') ? tags['Microsoft.Compute/virtualMachines'] : {} + userAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + virtualMachineName: managementVirtualMachineName + } +} + +module schedules 'schedules.bicep' = [for i in range(storageIndex, storageCount): { + name: 'Schedules_${i}_${timestamp}' + params: { + automationAccountName: automationAccount.name + fslogixContainerType: fslogixContainerType + storageAccountName: '${storageAccountNamePrefix}${padLeft(i, 2, '0')}' + timeZone: timeZone + } +}] + +module jobSchedules 'jobSchedules.bicep' = [for i in range(storageIndex, storageCount): { + name: 'JobSchedules_${i}_${timestamp}' + params: { + automationAccountName: automationAccount.name + environment: environment().name + fslogixContainerType: fslogixContainerType + runbookName: replace(runbookFileName, '.ps1', '') + resourceGroupName: storageResourceGroupName + storageAccountName: '${storageAccountNamePrefix}${padLeft(i, 2, '0')}' + subscriptionId: subscriptionId + timestamp: timestamp + } + dependsOn: [ + runbook + schedules + ] +}] + +module roleAssignment '../common/roleAssignment.bicep' = { + name: 'RoleAssignment_Storage_${timestamp}' + scope: resourceGroup(storageResourceGroupName) + params: { + PrincipalId: automationAccount.identity.principalId + PrincipalType: 'ServicePrincipal' + RoleDefinitionId: '17d1049b-9a84-46fb-8f53-869881c3d3ab' // Storage Account Contributor + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/automationAccount.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/automationAccount.bicep new file mode 100644 index 000000000..c9a09abf8 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/automationAccount.bicep @@ -0,0 +1,121 @@ +param automationAccountName string +param automationAccountPrivateDnsZoneResourceId string +param location string +param logAnalyticsWorkspaceResourceId string +param monitoring bool +param subnetResourceId string +param tags object +param virtualMachineName string + +var privateEndpointName = 'pe-${automationAccountName}-DSCAndHybridWorker' + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2023-07-01' existing = { + name: virtualMachineName +} + +resource automationAccount 'Microsoft.Automation/automationAccounts@2021-06-22' = { + name: automationAccountName + location: location + tags: contains(tags, 'Microsoft.Automation/automationAccounts') ? tags['Microsoft.Automation/automationAccounts'] : {} + identity: { + type: 'SystemAssigned' + } + properties: { + sku: { + name: 'Free' + } + } +} + +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = { + name: privateEndpointName + location: location + tags: contains(tags, 'Microsoft.Network/privateEndpoints') ? tags['Microsoft.Network/privateEndpoints'] : {} + properties: { + customNetworkInterfaceName: 'nic-${automationAccountName}-DSCAndHybridWorker' + privateLinkServiceConnections: [ + { + name: privateEndpointName + properties: { + privateLinkServiceId: automationAccount.id + groupIds: [ + 'DSCAndHybridWorker' + ] + } + } + ] + subnet: { + id: subnetResourceId + } + } +} + +resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = { + parent: privateEndpoint + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: replace(split(automationAccountPrivateDnsZoneResourceId, '/')[8], '.', '-') + properties: { + privateDnsZoneId: automationAccountPrivateDnsZoneResourceId + } + } + ] + } +} + +resource hybridRunbookWorkerGroup 'Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups@2022-08-08' = { + parent: automationAccount + name: 'Scaling Tool' +} + +resource hybridRunbookWorker 'Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/hybridRunbookWorkers@2022-08-08' = { + parent: hybridRunbookWorkerGroup + name: guid(hybridRunbookWorkerGroup.id) + properties: { + vmResourceId: virtualMachine.id + } +} + +resource extension_HybridWorker 'Microsoft.Compute/virtualMachines/extensions@2022-03-01' = { + parent: virtualMachine + name: 'HybridWorkerForWindows' + location: location + tags: contains(tags, 'Microsoft.Compute/virtualMachines') ? tags['Microsoft.Compute/virtualMachines'] : {} + properties: { + publisher: 'Microsoft.Azure.Automation.HybridWorker' + type: 'HybridWorkerForWindows' + typeHandlerVersion: '1.1' + autoUpgradeMinorVersion: true + enableAutomaticUpgrade: true + settings: { + AutomationAccountURL: automationAccount.properties.automationHybridServiceUrl + } + } +} + +// Enables logging in a log analytics workspace for alerting and dashboards +resource diagnostics 'Microsoft.Insights/diagnosticsettings@2017-05-01-preview' = if (monitoring) { + scope: automationAccount + name: 'diag-${automationAccountName}' + properties: { + logs: [ + { + category: 'DscNodeStatus' + enabled: true + } + { + category: 'JobLogs' + enabled: true + } + { + category: 'JobStreams' + enabled: true + } + ] + workspaceId: logAnalyticsWorkspaceResourceId + } +} + +output hybridRunbookWorkerGroupName string = hybridRunbookWorkerGroup.name diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/customerManagedKeys.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/customerManagedKeys.bicep new file mode 100644 index 000000000..ff3039b78 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/customerManagedKeys.bicep @@ -0,0 +1,171 @@ +param diskEncryptionKeyExpirationInDays int = 30 +param environment string +param keyVaultAbbreviation string +param keyVaultName string +param keyVaultPrivateDnsZoneResourceId string +param location string +param subnetResourceId string +param tags object +param timestamp string +param userAssignedIdentityNamePrefix string + +resource vault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: keyVaultName + location: location + tags: contains(tags, 'Microsoft.KeyVault/vaults') ? tags['Microsoft.KeyVault/vaults'] : {} + properties: { + enabledForDeployment: false + enabledForDiskEncryption: true + enabledForTemplateDeployment: false + enablePurgeProtection: true + enableRbacAuthorization: true + enableSoftDelete: true + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Deny' + ipRules: [] + virtualNetworkRules: [] + } + publicNetworkAccess: 'Disabled' + sku: { + family: 'A' + name: 'standard' + } + softDeleteRetentionInDays: environment == 'dev' || environment == 'test' ? 7 : 90 + tenantId: subscription().tenantId + } +} + +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = { + name: replace(keyVaultName, keyVaultAbbreviation, '${keyVaultAbbreviation}-pe') + location: location + tags: contains(tags, 'Microsoft.Network/privateEndpoints') ? tags['Microsoft.Network/privateEndpoints'] : {} + properties: { + customNetworkInterfaceName: replace(keyVaultName, keyVaultAbbreviation, '${keyVaultAbbreviation}-nic') + privateLinkServiceConnections: [ + { + name: replace(keyVaultName, keyVaultAbbreviation, '${keyVaultAbbreviation}-nic') + properties: { + privateLinkServiceId: vault.id + groupIds: [ + 'vault' + ] + } + } + ] + subnet: { + id: subnetResourceId + } + } +} + +resource privateDnsZoneGroups 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-08-01' = { + parent: privateEndpoint + name: keyVaultName + properties: { + privateDnsZoneConfigs: [ + { + name: 'ipconfig1' + properties: { + privateDnsZoneId: keyVaultPrivateDnsZoneResourceId + } + } + ] + } +} + +resource key_disks 'Microsoft.KeyVault/vaults/keys@2022-07-01' = { + parent: vault + name: 'DiskEncryptionKey' + properties: { + attributes: { + enabled: true + } + keySize: 4096 + kty: 'RSA' + rotationPolicy: { + attributes: { + expiryTime: 'P${string(diskEncryptionKeyExpirationInDays)}D' + } + lifetimeActions: [ + { + action: { + type: 'Notify' + } + trigger: { + timeBeforeExpiry: 'P10D' + } + } + { + action: { + type: 'Rotate' + } + trigger: { + timeAfterCreate: 'P${string(diskEncryptionKeyExpirationInDays - 7)}D' + } + } + ] + } + } +} + +resource key_storageAccounts 'Microsoft.KeyVault/vaults/keys@2022-07-01' = { + parent: vault + name: 'StorageEncryptionKey' + properties: { + attributes: { + enabled: true + } + keySize: 4096 + kty: 'RSA' + rotationPolicy: { + attributes: { + expiryTime: 'P${string(diskEncryptionKeyExpirationInDays)}D' + } + lifetimeActions: [ + { + action: { + type: 'Notify' + } + trigger: { + timeBeforeExpiry: 'P10D' + } + } + { + action: { + type: 'Rotate' + } + trigger: { + timeAfterCreate: 'P${string(diskEncryptionKeyExpirationInDays - 7)}D' + } + } + ] + } + } +} + +module userAssignedIdentity 'userAssignedIdentity.bicep' = { + name: 'UAI_Encryption_${timestamp}' + params: { + location: location + name: '${userAssignedIdentityNamePrefix}-encryption' + tags: contains(tags, 'Microsoft.ManagedIdentity/userAssignedIdentities') ? tags['Microsoft.ManagedIdentity/userAssignedIdentities'] : {} + } +} + +module roleAssignment '../common/roleAssignment.bicep' = { + name: 'RoleAssignment_Encryption_${timestamp}' + params: { + PrincipalId: userAssignedIdentity.outputs.principalId + PrincipalType: 'ServicePrincipal' + RoleDefinitionId: 'e147488a-f6f5-4113-8e2d-b22465e65bf6' // Key Vault Crypto Service Encryption User + } +} + +output keyUriWithVersion string = key_disks.properties.keyUriWithVersion +output keyVaultResourceId string = vault.id +output keyVaultUri string = vault.properties.vaultUri +output storageKeyName string = key_storageAccounts.name +output encryptionUserAssignedIdentityClientId string = userAssignedIdentity.outputs.clientId +output encryptionUserAssignedIdentityPrincipalId string = userAssignedIdentity.outputs.principalId +output encryptionUserAssignedIdentityResourceId string = userAssignedIdentity.outputs.resourceId diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/diskAccess.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/diskAccess.bicep new file mode 100644 index 000000000..3247304f0 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/diskAccess.bicep @@ -0,0 +1,37 @@ +param diskAccessName string +param location string +param subnetResourceId string +param tags object + +resource diskAccess 'Microsoft.Compute/diskAccesses@2021-04-01' = { + name: diskAccessName + location: location + tags: contains(tags, 'Microsoft.Compute/diskAccesses') ? tags['Microsoft.Compute/diskAccesses'] : {} + properties: {} +} + +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = { + name: 'pe-${diskAccessName}' + location: location + tags: contains(tags, 'Microsoft.Network/privateEndpoints') ? tags['Microsoft.Network/privateEndpoints'] : {} + properties: { + customNetworkInterfaceName: 'nic-${diskAccessName}' + privateLinkServiceConnections: [ + { + name: 'pe-${diskAccessName}' + properties: { + privateLinkServiceId: diskAccess.id + groupIds: [ + 'disks' + ] + } + } + ] + subnet: { + id: subnetResourceId + } + } +} + + +output resourceId string = diskAccess.id diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/diskEncryptionSet.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/diskEncryptionSet.bicep new file mode 100644 index 000000000..1803b7990 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/diskEncryptionSet.bicep @@ -0,0 +1,36 @@ +param diskEncryptionSetName string +param keyVaultResourceId string +param keyUrl string +param location string +param tags object +param timestamp string + +resource diskEncryptionSet 'Microsoft.Compute/diskEncryptionSets@2023-04-02' = { + name: diskEncryptionSetName + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + properties: { + activeKey: { + sourceVault: { + id: keyVaultResourceId + } + keyUrl: keyUrl + } + encryptionType: 'EncryptionAtRestWithPlatformAndCustomerKeys' + rotationToLatestKeyVersionEnabled: true + } +} + +module roleAssignment '../common/roleAssignment.bicep' = { + name: 'RoleAssignment_Encryption_${timestamp}' + params: { + PrincipalId: diskEncryptionSet.identity.principalId + PrincipalType: 'ServicePrincipal' + RoleDefinitionId: 'e147488a-f6f5-4113-8e2d-b22465e65bf6' // Key Vault Crypto Service Encryption User + } +} + +output resourceId string = diskEncryptionSet.id diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/jobSchedules.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/jobSchedules.bicep new file mode 100644 index 000000000..0a73a09dc --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/jobSchedules.bicep @@ -0,0 +1,54 @@ +param automationAccountName string +param environment string +param fslogixContainerType string +param resourceGroupName string +param runbookName string +param storageAccountName string +param subscriptionId string +param timestamp string + +resource automationAccount 'Microsoft.Automation/automationAccounts@2022-08-08' existing = { + name: automationAccountName +} + +resource jobSchedules_ProfileContainers 'Microsoft.Automation/automationAccounts/jobSchedules@2022-08-08' = [for i in range(0, 4): { + parent: automationAccount + name: guid(timestamp, runbookName, storageAccountName, 'ProfileContainers', string(i)) + properties: { + parameters: { + environment: environment + FileShareName: 'profile-containers' + resourceGroupName: resourceGroupName + storageAccountName: storageAccountName + subscriptionId: subscriptionId + } + runbook: { + name: runbookName + } + runOn: null + schedule: { + name: '${storageAccountName}_ProfileContainers_${(i + 1) * 15}min' + } + } +}] + +resource jobSchedules_OfficeContainers 'Microsoft.Automation/automationAccounts/jobSchedules@2022-08-08' = [for i in range(0, 4): if (contains(fslogixContainerType, 'Office')) { + parent: automationAccount + name: guid(timestamp, runbookName, storageAccountName, 'OfficeContainers', string(i)) + properties: { + parameters: { + environment: environment + FileShareName: 'office-containers' + resourceGroupName: resourceGroupName + storageAccountName: storageAccountName + subscriptionId: subscriptionId + } + runbook: { + name: runbookName + } + runOn: null + schedule: { + name: '${storageAccountName}_OfficeContainers_${(i + 1) * 15}min' + } + } +}] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/management.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/management.bicep new file mode 100644 index 000000000..635789f27 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/management.bicep @@ -0,0 +1,339 @@ +targetScope = 'subscription' + +param activeDirectorySolution string +param artifactsUri string +param artifactsStorageAccountResourceId string +param automationAccountName string +param automationAccountPrivateDnsZoneResourceId string +param availability string +param avdObjectId string +param azureBlobsPrivateDnsZoneResourceId string +param azurePowerShellModuleMsiName string +param azureQueueStoragePrivateDnsZoneResourceId string +param dataCollectionRuleName string +//param diskAccessName string +param diskNamePrefix string +param diskEncryptionSetName string +param diskSku string +@secure() +param domainJoinPassword string +param domainJoinUserPrincipalName string +param domainName string +param enableMonitoring bool +param environmentShortName string +param fslogix bool +param fslogixStorageService string +param hostPoolName string +param hostPoolType string +param imageDefinitionResourceId string +param keyVaultAbbreviation string +param keyVaultName string +param keyVaultPrivateDnsZoneResourceId string +param locationVirtualMachines string +param logAnalyticsWorkspaceName string +param logAnalyticsWorkspaceRetention int +param logAnalyticsWorkspaceSku string +param networkInterfaceNamePrefix string +param organizationalUnitPath string +param recoveryServices bool +param recoveryServicesPrivateDnsZoneResourceId string +param recoveryServicesVaultName string +param resourceGroupControlPlane string +param resourceGroupFeedWorkspace string +param resourceGroupHosts string +param resourceGroupManagement string +param resourceGroupStorage string +param roleDefinitions object +param scalingTool bool +param securityLogAnalyticsWorkspaceResourceId string +param sessionHostCount int +param storageService string +param subnetResourceId string +param tags object +param timestamp string +param timeZone string +param userAssignedIdentityNamePrefix string +param virtualMachineMonitoringAgent string +param virtualMachineNamePrefix string +@secure() +param virtualMachinePassword string +param virtualMachineUsername string +param virtualMachineSize string +param workspaceNamePrefix string + +var CpuCountMax = contains(hostPoolType, 'Pooled') ? 32 : 128 +var CpuCountMin = contains(hostPoolType, 'Pooled') ? 4 : 2 +var roleAssignments = union(roleAssignmentsCommon, roleAssignmentStorage) +var roleAssignmentsCommon = [ + { + roleDefinitionId: 'f353d9bd-d4a6-484e-a77a-8050b599b867' // Automation Contributor (Purpose: adds runbook to automation account) + resourceGroup: resourceGroupManagement + subscription: subscription().subscriptionId + } + { + roleDefinitionId: '86240b0e-9422-4c43-887b-b61143f32ba8' // Desktop Virtualization Application Group Contributor (Purpose: updates the friendly name for the desktop) + resourceGroup: resourceGroupControlPlane + subscription: subscription().subscriptionId + } + { + roleDefinitionId: '2ad6aaab-ead9-4eaa-8ac5-da422f562408' // Desktop Virtualization Session Host Operator (Purpose: sets drain mode on the AVD session hosts) + resourceGroup: resourceGroupControlPlane + subscription: subscription().subscriptionId + } + { + roleDefinitionId: 'a959dbd1-f747-45e3-8ba6-dd80f235f97c' // Desktop Virtualization Virtual Machine Contributor (Purpose: remove the management virtual machine) + resourceGroup: resourceGroupManagement + subscription: subscription().subscriptionId + } + { + roleDefinitionId: '21efdde3-836f-432b-bf3d-3e8e734d4b2b' // Desktop Virtualization Workspace Contributor (Purpose: update the app group references on an existing feed workspace) + resourceGroup: resourceGroupFeedWorkspace + subscription: subscription().subscriptionId + } + { + roleDefinitionId: '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' // Storage Blob Data Reader + resourceGroup: split(artifactsStorageAccountResourceId, '/')[4] + subscription: split(artifactsStorageAccountResourceId, '/')[2] + } +] +var roleAssignmentStorage = fslogix ? [ + { + roleDefinitionId: '17d1049b-9a84-46fb-8f53-869881c3d3ab' // Storage Account Contributor (Purpose: domain join storage account & set NTFS permissions on the file share) + resourceGroup: resourceGroupStorage + subscription: subscription().subscriptionId + } +] : [] +var VirtualNetworkName = split(subnetResourceId, '/')[8] +var VirtualNetworkResourceGroupName = split(subnetResourceId, '/')[4] + +// Disabling the deployment below until Enhanced Policies in Recovery Services support managed disks with private link +/* module diskAccess 'diskAccess.bicep' = { + scope: resourceGroup(resourceGroupManagement) + name: 'DiskAccess_${timestamp}' + params: { + diskAccessName: diskAccessName + location: locationVirtualMachines + subnetResourceId: subnetResourceId + tags: tags + } +} */ + +// Sets an Azure policy to disable public network access to managed disks +// Once Enhanced Policies in Recovery Services support managed disks with private link, remove the "if" condition +module policy 'policy.bicep' = if (contains(hostPoolType, 'Pooled') && recoveryServices) { + name: 'Policy_${timestamp}' + params: { + // Disabling the param below until Enhanced Policies in Recovery Services support managed disks with private link + //diskAccessResourceId: diskAccess.outputs.resourceId + location: locationVirtualMachines + resourceGroupName: resourceGroupHosts + } +} + +module deploymentUserAssignedIdentity 'userAssignedIdentity.bicep' = { + scope: resourceGroup(resourceGroupManagement) + name: 'UserAssignedIdentity_${timestamp}' + params: { + location: locationVirtualMachines + name: '${userAssignedIdentityNamePrefix}-deployment' + tags: contains(tags, 'Microsoft.ManagedIdentity/userAssignedIdentities') ? tags['Microsoft.ManagedIdentity/userAssignedIdentities'] : {} + } +} + +module roleAssignments_deployment '../common/roleAssignment.bicep' = [for i in range(0, length(roleAssignments)): { + scope: resourceGroup(roleAssignments[i].subscription, roleAssignments[i].resourceGroup) + name: 'RoleAssignment_${i}_${timestamp}' + params: { + PrincipalId: deploymentUserAssignedIdentity.outputs.principalId + PrincipalType: 'ServicePrincipal' + RoleDefinitionId: roleAssignments[i].roleDefinitionId + } +}] + +// Role Assignment for Validation +// This role assignment is required to collect validation information +resource roleAssignment_validation 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid('${userAssignedIdentityNamePrefix}-deployment', roleDefinitions.Reader, subscription().id) + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.Reader) + principalId: deploymentUserAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + } +} + +module artifacts 'artifacts.bicep' = { + scope: resourceGroup(split(artifactsStorageAccountResourceId, '/')[2], split(artifactsStorageAccountResourceId, '/')[4]) + name: 'Artifacts_${timestamp}' + params: { + location: locationVirtualMachines + resourceGroupManagement: resourceGroupManagement + storageAccountName: split(artifactsStorageAccountResourceId, '/')[8] + subscriptionId: subscription().subscriptionId + tags: tags + timestamp: timestamp + userAssignedIdentityNamePrefix: userAssignedIdentityNamePrefix + } +} + +// Deploys the prerequisites to enable customer managed keys on storage accounts and managed disks +module customerManagedKeys 'customerManagedKeys.bicep' = { + name: 'CustomerManagedKeys_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + environment: environmentShortName + keyVaultAbbreviation: keyVaultAbbreviation + keyVaultName: keyVaultName + keyVaultPrivateDnsZoneResourceId: keyVaultPrivateDnsZoneResourceId + location: locationVirtualMachines + subnetResourceId: subnetResourceId + tags: tags + timestamp: timestamp + userAssignedIdentityNamePrefix: userAssignedIdentityNamePrefix + } +} + +module diskEncryptionSet 'diskEncryptionSet.bicep' = { + name: 'DiskEncryptionSet_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + diskEncryptionSetName: diskEncryptionSetName + keyUrl: customerManagedKeys.outputs.keyUriWithVersion + keyVaultResourceId: customerManagedKeys.outputs.keyVaultResourceId + location: locationVirtualMachines + tags: contains(tags, 'Microsoft.Compute/diskEncryptionSets') ? tags['Microsoft.Compute/diskEncryptionSets'] : {} + timestamp: timestamp + } +} + +// Management VM +// The management VM is required to validate the deployment and configure FSLogix storage. +module virtualMachine 'virtualMachine.bicep' = { + name: 'ManagementVirtualMachine_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + artifactsUri: artifactsUri + azurePowerShellModuleMsiName: azurePowerShellModuleMsiName + deploymentUserAssignedIdentityClientId: deploymentUserAssignedIdentity.outputs.clientId + deploymentUserAssignedIdentityResourceId: deploymentUserAssignedIdentity.outputs.resourceId + diskEncryptionSetResourceId: diskEncryptionSet.outputs.resourceId + diskNamePrefix: diskNamePrefix + diskSku: diskSku + domainJoinPassword: domainJoinPassword + domainJoinUserPrincipalName: domainJoinUserPrincipalName + domainName: domainName + location: locationVirtualMachines + networkInterfaceNamePrefix: networkInterfaceNamePrefix + organizationalUnitPath: organizationalUnitPath + securityLogAnalyticsWorkspaceResourceId: securityLogAnalyticsWorkspaceResourceId + subnet: split(subnetResourceId, '/')[10] + tagsNetworkInterfaces: contains(tags, 'Microsoft.Network/networkInterfaces') ? tags['Microsoft.Network/networkInterfaces'] : {} + tagsVirtualMachines: contains(tags, 'Microsoft.Compute/virtualMachines') ? tags['Microsoft.Compute/virtualMachines'] : {} + virtualMachineMonitoringAgent: virtualMachineMonitoringAgent + virtualMachineNamePrefix: virtualMachineNamePrefix + virtualMachinePassword: virtualMachinePassword + virtualMachineUsername: virtualMachineUsername + virtualNetwork: VirtualNetworkName + virtualNetworkResourceGroup: VirtualNetworkResourceGroupName + } +} + +// Deployment Validations +// This module validates the selected parameter values and collects required data +module validations '../common/customScriptExtensions.bicep' = { + scope: resourceGroup(resourceGroupManagement) + name: 'Validations_${timestamp}' + params: { + fileUris: [ + '${artifactsUri}Get-Validations.ps1' + ] + location: locationVirtualMachines + parameters: '-ActiveDirectorySolution ${activeDirectorySolution} -CpuCountMax ${CpuCountMax} -CpuCountMin ${CpuCountMin} -DomainName ${empty(domainName) ? 'NotApplicable' : domainName} -Environment ${environment().name} -ImageDefinitionResourceId ${empty(imageDefinitionResourceId) ? 'NotApplicable' : imageDefinitionResourceId} -Location ${locationVirtualMachines} -SessionHostCount ${sessionHostCount} -StorageService ${storageService} -SubscriptionId ${subscription().subscriptionId} -TenantId ${tenant().tenantId} -UserAssignedIdentityClientId ${deploymentUserAssignedIdentity.outputs.clientId} -VirtualMachineSize ${virtualMachineSize} -VirtualNetworkName ${VirtualNetworkName} -VirtualNetworkResourceGroupName ${VirtualNetworkResourceGroupName} -WorkspaceNamePrefix ${workspaceNamePrefix} -WorkspaceResourceGroupName ${resourceGroupFeedWorkspace}' + scriptFileName: 'Get-Validations.ps1' + tags: contains(tags, 'Microsoft.Compute/virtualMachines') ? tags['Microsoft.Compute/virtualMachines'] : {} + userAssignedIdentityClientId: deploymentUserAssignedIdentity.outputs.clientId + virtualMachineName: virtualMachine.outputs.Name + } +} + +// Role Assignment required for Start VM On Connect +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(avdObjectId, roleDefinitions.DesktopVirtualizationPowerOnContributor, subscription().id) + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.DesktopVirtualizationPowerOnContributor) + principalId: avdObjectId + } +} + +// Monitoring Resources for AVD Insights +// This module deploys a Log Analytics Workspace with either Windows Events & Windows Performance Counters or a Data Collection Rule +module monitoring 'monitoring.bicep' = if (enableMonitoring) { + name: 'Monitoring_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + dataCollectionRuleName: dataCollectionRuleName + hostPoolName: hostPoolName + location: locationVirtualMachines + logAnalyticsWorkspaceName: logAnalyticsWorkspaceName + logAnalyticsWorkspaceRetention: logAnalyticsWorkspaceRetention + logAnalyticsWorkspaceSku: logAnalyticsWorkspaceSku + resourceGroupControlPlane: resourceGroupControlPlane + tags: tags + virtualMachineMonitoringAgent: virtualMachineMonitoringAgent + } +} + +// Automation Account required for the AVD Scaling Tool and the Auto Increase Premium File Share Quota solution +module automationAccount 'automationAccount.bicep' = if (scalingTool || fslogixStorageService == 'AzureFiles Premium') { + name: 'AutomationAccount_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + automationAccountName: automationAccountName + automationAccountPrivateDnsZoneResourceId: automationAccountPrivateDnsZoneResourceId + location: locationVirtualMachines + logAnalyticsWorkspaceResourceId: enableMonitoring ? monitoring.outputs.logAnalyticsWorkspaceResourceId : '' + monitoring: enableMonitoring + subnetResourceId: subnetResourceId + tags: contains(tags, 'Microsoft.Automation/automationAccounts') ? tags['Microsoft.Automation/automationAccounts'] : {} + virtualMachineName: virtualMachine.outputs.Name + } +} + +module recoveryServicesVault 'recoveryServicesVault.bicep' = if (recoveryServices && ((contains(activeDirectorySolution, 'DomainServices') && contains(hostPoolType, 'Pooled') && contains(fslogixStorageService, 'AzureFiles')) || contains(hostPoolType, 'Personal'))) { + name: 'RecoveryServicesVault_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + azureBlobsPrivateDnsZoneResourceId: azureBlobsPrivateDnsZoneResourceId + fslogix: fslogix + location: locationVirtualMachines + azureQueueStoragePrivateDnsZoneResourceId: azureQueueStoragePrivateDnsZoneResourceId + recoveryServicesPrivateDnsZoneResourceId: recoveryServicesPrivateDnsZoneResourceId + recoveryServicesVaultName: recoveryServicesVaultName + storageService: storageService + subnetId: subnetResourceId + tags: tags + timeZone: timeZone + } +} + +output artifactsUserAssignedIdentityClientId string = artifacts.outputs.userAssignedIdentityClientId +output artifactsUserAssignedIdentityPrincipalId string = artifacts.outputs.userAssignedIdentityPrincipalId +output artifactsUserAssignedIdentityResourceId string = artifacts.outputs.userAssignedIdentityResourceId +output dataCollectionRuleResourceId string = enableMonitoring ? monitoring.outputs.dataCollectionRuleResourceId : '' +output deploymentUserAssignedIdentityClientId string = deploymentUserAssignedIdentity.outputs.clientId +output deploymentUserAssignedIdentityPrincipalId string = deploymentUserAssignedIdentity.outputs.principalId +output deploymentUserAssignedIdentityResourceId string = deploymentUserAssignedIdentity.outputs.resourceId +output diskEncryptionSetResourceId string = diskEncryptionSet.outputs.resourceId +output encryptionUserAssignedIdentityClientId string = customerManagedKeys.outputs.encryptionUserAssignedIdentityClientId +output encryptionUserAssignedIdentityPrincipalId string = customerManagedKeys.outputs.encryptionUserAssignedIdentityPrincipalId +output encryptionUserAssignedIdentityResourceId string = customerManagedKeys.outputs.encryptionUserAssignedIdentityResourceId +output existingFeedWorkspace bool = validations.outputs.value.existingWorkspace == 'true' ? true : false +output hybridRunbookWorkerGroupName string = scalingTool || fslogixStorageService == 'AzureFiles Premium' ? automationAccount.outputs.hybridRunbookWorkerGroupName : '' +output keyVaultUri string = customerManagedKeys.outputs.keyVaultUri +output logAnalyticsWorkspaceResourceId string = enableMonitoring ? monitoring.outputs.logAnalyticsWorkspaceResourceId : '' +output storageEncryptionKeyName string = customerManagedKeys.outputs.storageKeyName +output validateAcceleratedNetworking string = validations.outputs.value.acceleratedNetworking +output validateANFDnsServers string = validations.outputs.value.anfDnsServers +output validateANFfActiveDirectory string = validations.outputs.value.anfActiveDirectory +output validateANFSubnetId string = validations.outputs.value.anfSubnetId +output validateAvailabilityZones array = availability == 'AvailabilityZones' ? validations.outputs.value.availabilityZones : [ '1' ] +output virtualMachineName string = virtualMachine.outputs.Name diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/monitoring.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/monitoring.bicep new file mode 100644 index 000000000..4badbbfd1 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/monitoring.bicep @@ -0,0 +1,536 @@ +param dataCollectionRuleName string +param hostPoolName string +param location string +param logAnalyticsWorkspaceName string +param logAnalyticsWorkspaceRetention int +param logAnalyticsWorkspaceSku string +param resourceGroupControlPlane string +param tags object +param virtualMachineMonitoringAgent string + +var WindowsEvents = [ + { + name: 'Microsoft-FSLogix-Apps/Operational' + types: [ + { + eventType: 'Error' + } + { + eventType: 'Warning' + } + { + eventType: 'Information' + } + ] + } + { + name: 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational' + types: [ + { + eventType: 'Error' + } + { + eventType: 'Warning' + } + { + eventType: 'Information' + } + ] + } + { + name: 'System' + types: [ + { + eventType: 'Error' + } + { + eventType: 'Warning' + } + ] + } + { + name: 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Admin' + types: [ + { + eventType: 'Error' + } + { + eventType: 'Warning' + } + { + eventType: 'Information' + } + ] + } + { + name: 'Microsoft-FSLogix-Apps/Admin' + types: [ + { + eventType: 'Error' + } + { + eventType: 'Warning' + } + { + eventType: 'Information' + } + ] + } + { + name: 'Application' + types: [ + { + eventType: 'Error' + } + { + eventType: 'Warning' + } + ] + } +] +var WindowsPerformanceCounters = [ + { + objectName: 'LogicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Disk Transfers/sec' + } + { + objectName: 'LogicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Current Disk Queue Length' + } + { + objectName: 'LogicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Disk Reads/sec' + } + { + objectName: 'LogicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: '% Free Space' + } + { + objectName: 'LogicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Avg. Disk sec/Read' + } + { + objectName: 'LogicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Disk Writes/sec' + } + { + objectName: 'LogicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Avg. Disk sec/Write' + } + { + objectName: 'LogicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Free Megabytes' + } + { + objectName: 'LogicalDisk' + instanceName: 'C:' + intervalSeconds: 60 + counterName: '% Free Space' + } + { + objectName: 'LogicalDisk' + instanceName: 'C:' + intervalSeconds: 30 + counterName: 'Avg. Disk Queue Length' + } + { + objectName: 'LogicalDisk' + instanceName: 'C:' + intervalSeconds: 60 + counterName: 'Avg. Disk sec/Transfer' + } + { + objectName: 'LogicalDisk' + instanceName: 'C:' + intervalSeconds: 30 + counterName: 'Current Disk Queue Length' + } + { + objectName: 'Memory' + instanceName: '*' + intervalSeconds: 60 + counterName: '% Committed Bytes In Use' + } + { + objectName: 'Memory' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Available MBytes' + } + { + objectName: 'Memory' + instanceName: '*' + intervalSeconds: 30 + counterName: 'Available Mbytes' + } + { + objectName: 'Memory' + instanceName: '*' + intervalSeconds: 30 + counterName: 'Page Faults/sec' + } + { + objectName: 'Memory' + instanceName: '*' + intervalSeconds: 30 + counterName: 'Pages/sec' + } + { + objectName: 'Network Adapter' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Bytes Sent/sec' + } + { + objectName: 'Network Adapter' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Bytes Received/sec' + } + { + objectName: 'Network Interface' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Bytes Total/sec' + } + { + objectName: 'PhysicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Avg. Disk Bytes/Transfer' + } + { + objectName: 'PhysicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Avg. Disk Bytes/Read' + } + { + objectName: 'PhysicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Avg. Disk sec/Write' + } + { + objectName: 'PhysicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Avg. Disk sec/Read' + } + { + objectName: 'PhysicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Avg. Disk Bytes/Write' + } + { + objectName: 'PhysicalDisk' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Avg. Disk sec/Transfer' + } + { + objectName: 'PhysicalDisk' + instanceName: '*' + intervalSeconds: 30 + counterName: 'Avg. Disk Queue Length' + } + { + objectName: 'Process' + instanceName: '*' + intervalSeconds: 60 + counterName: 'IO Write Operations/sec' + } + { + objectName: 'Process' + instanceName: '*' + intervalSeconds: 60 + counterName: 'IO Read Operations/sec' + } + { + objectName: 'Process' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Thread Count' + } + { + objectName: 'Process' + instanceName: '*' + intervalSeconds: 60 + counterName: '% User Time' + } + { + objectName: 'Process' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Working Set' + } + { + objectName: 'Process' + instanceName: '*' + intervalSeconds: 60 + counterName: '% Processor Time' + } + { + objectName: 'Processor' + instanceName: '_Total' + intervalSeconds: 60 + counterName: '% Processor Time' + } + { + objectName: 'Processor Information' + instanceName: '_Total' + intervalSeconds: 30 + counterName: '% Processor Time' + } + { + objectName: 'RemoteFX Graphics' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Frames Skipped/Second - Insufficient Server Resources' + } + { + objectName: 'RemoteFX Graphics' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Average Encoding Time' + } + { + objectName: 'RemoteFX Graphics' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Frames Skipped/Second - Insufficient Client Resources' + } + { + objectName: 'RemoteFX Graphics' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Frames Skipped/Second - Insufficient Network Resources' + } + { + objectName: 'RemoteFX Network' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Current UDP Bandwidth' + } + { + objectName: 'RemoteFX Network' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Current TCP Bandwidth' + } + { + objectName: 'RemoteFX Network' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Current TCP RTT' + } + { + objectName: 'RemoteFX Network' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Current UDP RTT' + } + { + objectName: 'System' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Processor Queue Length' + } + { + objectName: 'Terminal Services' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Inactive Sessions' + } + { + objectName: 'Terminal Services' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Total Sessions' + } + { + objectName: 'Terminal Services' + instanceName: '*' + intervalSeconds: 60 + counterName: 'Active Sessions' + } + { + objectName: 'Terminal Services Session' + instanceName: '*' + intervalSeconds: 60 + counterName: '% Processor Time' + } + { + objectName: 'User Input Delay per Process' + instanceName: '*' + intervalSeconds: 30 + counterName: 'Max Input Delay' + } + { + objectName: 'User Input Delay per Session' + instanceName: '*' + intervalSeconds: 30 + counterName: 'Max Input Delay' + } +] + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-06-01' = { + name: logAnalyticsWorkspaceName + location: location + tags: union({ + 'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupControlPlane}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}' + }, contains(tags, 'Microsoft.OperationalInsights/workspaces') ? tags['Microsoft.OperationalInsights/workspaces'] : {}) + properties: { + sku: { + name: logAnalyticsWorkspaceSku + } + retentionInDays: logAnalyticsWorkspaceRetention + workspaceCapping: { + dailyQuotaGb: -1 + } + publicNetworkAccessForIngestion: 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + } +} + +@batchSize(1) +resource windowsEvents 'Microsoft.OperationalInsights/workspaces/dataSources@2020-08-01' = [for (item, i) in WindowsEvents: if (virtualMachineMonitoringAgent == 'LogAnalyticsAgent') { + parent: logAnalyticsWorkspace + name: 'WindowsEvent${i}' + tags: union({ + 'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupControlPlane}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}' + }, contains(tags, 'Microsoft.OperationalInsights/workspaces') ? tags['Microsoft.OperationalInsights/workspaces'] : {}) + kind: 'WindowsEvent' + properties: { + eventLogName: item.name + eventTypes: item.types + } +}] + +@batchSize(1) +resource windowsPerformanceCounters 'Microsoft.OperationalInsights/workspaces/dataSources@2020-08-01' = [for (item, i) in WindowsPerformanceCounters: if (virtualMachineMonitoringAgent == 'LogAnalyticsAgent') { + parent: logAnalyticsWorkspace + name: 'WindowsPerformanceCounter${i}' + tags: union({ + 'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupControlPlane}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}' + }, contains(tags, 'Microsoft.OperationalInsights/workspaces') ? tags['Microsoft.OperationalInsights/workspaces'] : {}) + kind: 'WindowsPerformanceCounter' + properties: { + objectName: item.objectName + instanceName: item.instanceName + intervalSeconds: item.intervalSeconds + counterName: item.counterName + } + dependsOn: [ + windowsEvents + ] +}] + +resource dataCollectionRule 'Microsoft.Insights/dataCollectionRules@2022-06-01' = if (virtualMachineMonitoringAgent == 'AzureMonitorAgent') { + name: dataCollectionRuleName + location: location + tags: union({ + 'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupControlPlane}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}' + }, contains(tags, 'Microsoft.Insights/dataCollectionRules') ? tags['Microsoft.Insights/dataCollectionRules'] : {}) + kind: 'Windows' + properties: { + dataSources: { + performanceCounters: [ + { + streams: [ + 'Microsoft-Perf' + ] + samplingFrequencyInSeconds: 30 + counterSpecifiers: [ + '\\LogicalDisk(C:)\\Avg. Disk Queue Length' + '\\LogicalDisk(C:)\\Current Disk Queue Length' + '\\Memory\\Available Mbytes' + '\\Memory\\Page Faults/sec' + '\\Memory\\Pages/sec' + '\\Memory\\% Committed Bytes In Use' + '\\PhysicalDisk(*)\\Avg. Disk Queue Length' + '\\PhysicalDisk(*)\\Avg. Disk sec/Read' + '\\PhysicalDisk(*)\\Avg. Disk sec/Transfer' + '\\PhysicalDisk(*)\\Avg. Disk sec/Write' + '\\Processor Information(_Total)\\% Processor Time' + '\\User Input Delay per Process(*)\\Max Input Delay' + '\\User Input Delay per Session(*)\\Max Input Delay' + '\\RemoteFX Network(*)\\Current TCP RTT' + '\\RemoteFX Network(*)\\Current UDP Bandwidth' + ] + name: 'perfCounterDataSource10' + } + { + streams: [ + 'Microsoft-Perf' + ] + samplingFrequencyInSeconds: 60 + counterSpecifiers: [ + '\\LogicalDisk(C:)\\% Free Space' + '\\LogicalDisk(C:)\\Avg. Disk sec/Transfer' + '\\Terminal Services(*)\\Active Sessions' + '\\Terminal Services(*)\\Inactive Sessions' + '\\Terminal Services(*)\\Total Sessions' + ] + name: 'perfCounterDataSource30' + } + ] + windowsEventLogs: [ + { + streams: [ + 'Microsoft-Event' + ] + xPathQueries: [ + 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Admin!*[System[(Level=2 or Level=3 or Level=4 or Level=0)]]' + 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational!*[System[(Level=2 or Level=3 or Level=4 or Level=0)]]' + 'System!*' + 'Microsoft-FSLogix-Apps/Operational!*[System[(Level=2 or Level=3 or Level=4 or Level=0)]]' + 'Application!*[System[(Level=2 or Level=3)]]' + 'Microsoft-FSLogix-Apps/Admin!*[System[(Level=2 or Level=3 or Level=4 or Level=0)]]' + ] + name: 'eventLogsDataSource' + } + ] + } + destinations: { + logAnalytics: [ + { + workspaceResourceId: logAnalyticsWorkspace.id + name: 'la-workspace' + } + ] + } + dataFlows: [ + { + streams: [ + 'Microsoft-Perf' + 'Microsoft-Event' + ] + destinations: [ + 'la-workspace' + ] + } + ] + } +} + +output logAnalyticsWorkspaceResourceId string = logAnalyticsWorkspace.id +output dataCollectionRuleResourceId string = virtualMachineMonitoringAgent == 'AzureMonitorAgent' ? dataCollectionRule.id : '' + diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/policy.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/policy.bicep new file mode 100644 index 000000000..62455c83a --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/policy.bicep @@ -0,0 +1,71 @@ +targetScope = 'subscription' + +// Disabling the param below until Enhanced Policies in Recovery Services support managed disks with private link +//param diskAccessResourceId string +param location string +param resourceGroupName string + +resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = { + name: 'DiskNetworkAccess' + properties: { + description: 'Disable network access to managed disks in the ${resourceGroupName} resource group' + displayName: 'Disable Disk Access (${resourceGroupName})' + mode: 'All' + parameters: {} + policyRule: { + if: { + field: 'type' + equals: 'Microsoft.Compute/disks' + } + then: { + effect: 'modify' + details: { + roleDefinitionIds: [ + '/providers/Microsoft.Authorization/roleDefinitions/60fc6e62-5479-42d4-8bf4-67625fcc2840' + ] + operations: [ + { + operation: 'addOrReplace' + field: 'Microsoft.Compute/disks/networkAccessPolicy' + value: 'DenyAll' + } + { + operation: 'addOrReplace' + field: 'Microsoft.Compute/disks/publicNetworkAccess' + value: 'Disabled' + } + // Disabling the configuration below until Enhanced Policies in Recovery Services support managed disks with private link + // Once it is supported, these settings would replace the settings / operations above + /* { + operation: 'addOrReplace' + field: 'Microsoft.Compute/disks/networkAccessPolicy' + value: 'AllowPrivate' + } + { + operation: 'addOrReplace' + field: 'Microsoft.Compute/disks/publicNetworkAccess' + value: 'Disabled' + } + { + operation: 'addOrReplace' + field: 'Microsoft.Compute/disks/diskAccessId' + value: diskAccessResourceId + } */ + ] + } + } + } + policyType: 'Custom' + } +} + +module policyAssignment 'policyAssignment.bicep' = { + name: 'DiskNetworkAccess' + scope: resourceGroup(resourceGroupName) + params: { + location: location + policyDefinitionId: policyDefinition.id + policyDisplayName: policyDefinition.properties.displayName + policyName: policyDefinition.properties.displayName + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/policyAssignment.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/policyAssignment.bicep new file mode 100644 index 000000000..c0994be90 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/policyAssignment.bicep @@ -0,0 +1,17 @@ +param location string +param policyDefinitionId string +param policyDisplayName string +param policyName string + +resource policyAssignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = { + name: policyName + scope: resourceGroup() + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + displayName: policyDisplayName + policyDefinitionId: policyDefinitionId + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/recoveryServicesVault.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/recoveryServicesVault.bicep new file mode 100644 index 000000000..ea684d69c --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/recoveryServicesVault.bicep @@ -0,0 +1,139 @@ +param azureBlobsPrivateDnsZoneResourceId string +param azureQueueStoragePrivateDnsZoneResourceId string +param fslogix bool +param location string +param recoveryServicesPrivateDnsZoneResourceId string +param recoveryServicesVaultName string +param storageService string +param subnetId string +param tags object +param timeZone string + +resource vault 'Microsoft.RecoveryServices/vaults@2022-03-01' = { + name: recoveryServicesVaultName + location: location + tags: contains(tags, 'Microsoft.RecoveryServices/vaults') ? tags['Microsoft.RecoveryServices/vaults'] : {} + sku: { + name: 'RS0' + tier: 'Standard' + } + properties: {} +} + +resource backupPolicy_Storage 'Microsoft.RecoveryServices/vaults/backupPolicies@2022-03-01' = if (fslogix && storageService == 'AzureFiles') { + parent: vault + name: 'AvdPolicyStorage' + location: location + tags: contains(tags, 'Microsoft.RecoveryServices/vaults') ? tags['Microsoft.RecoveryServices/vaults'] : {} + properties: { + backupManagementType: 'AzureStorage' + schedulePolicy: { + scheduleRunFrequency: 'Daily' + scheduleRunTimes: [ + '23:00' + ] + schedulePolicyType: 'SimpleSchedulePolicy' + } + retentionPolicy: { + retentionPolicyType: 'LongTermRetentionPolicy' + dailySchedule: { + retentionTimes: [ + '23:00' + ] + retentionDuration: { + count: 30 + durationType: 'Days' + } + } + } + timeZone: timeZone + workLoadType: 'AzureFileShare' + } +} + +resource backupPolicy_Vm 'Microsoft.RecoveryServices/vaults/backupPolicies@2022-03-01' = if (!fslogix) { + parent: vault + name: 'AvdPolicyVm' + location: location + tags: contains(tags, 'Microsoft.RecoveryServices/vaults') ? tags['Microsoft.RecoveryServices/vaults'] : {} + properties: { + backupManagementType: 'AzureIaasVM' + instantRpRetentionRangeInDays: 2 + policyType: 'V2' + retentionPolicy: { + retentionPolicyType: 'LongTermRetentionPolicy' + dailySchedule: { + retentionTimes: [ + '23:00' + ] + retentionDuration: { + count: 30 + durationType: 'Days' + } + } + } + schedulePolicy: { + schedulePolicyType: 'SimpleSchedulePolicyV2' + scheduleRunFrequency: 'Daily' + dailySchedule: { + scheduleRunTimes: [ + '23:00' + ] + } + } + timeZone: timeZone + } +} + +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = { + name: 'pe-${recoveryServicesVaultName}' + location: location + tags: contains(tags, 'Microsoft.Network/privateEndpoints') ? tags['Microsoft.Network/privateEndpoints'] : {} + properties: { + customNetworkInterfaceName: 'nic-${recoveryServicesVaultName}' + privateLinkServiceConnections: [ + { + name: 'pe-${recoveryServicesVaultName}' + properties: { + privateLinkServiceId: vault.id + groupIds: [ + 'AzureBackup' + ] + } + } + ] + subnet: { + id: subnetId + } + } +} + +resource privateDnsZoneGroups 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2021-08-01' = { + parent: privateEndpoint + name: recoveryServicesVaultName + properties: { + privateDnsZoneConfigs: [ + { + name: replace(recoveryServicesPrivateDnsZoneResourceId, '.', '-') + properties: { + privateDnsZoneId: recoveryServicesPrivateDnsZoneResourceId + } + } + { + name: replace(azureQueueStoragePrivateDnsZoneResourceId, '.', '-') + properties: { + privateDnsZoneId: azureQueueStoragePrivateDnsZoneResourceId + } + } + { + name: replace(azureBlobsPrivateDnsZoneResourceId, '.', '-') + properties: { + privateDnsZoneId: azureBlobsPrivateDnsZoneResourceId + } + } + ] + } + dependsOn: [ + vault + ] +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/scalingTool.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/scalingTool.bicep new file mode 100644 index 000000000..259afb159 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/scalingTool.bicep @@ -0,0 +1,106 @@ +param artifactsUri string +param automationAccountName string +param beginPeakTime string +param endPeakTime string +param hostPoolName string +param hostPoolResourceGroupName string +param hybridRunbookWorkerGroupName string +param limitSecondsToForceLogOffUser string +param location string +param managementVirtualMachineName string +param minimumNumberOfRdsh string +param resourceGroupControlPlane string +param resourceGroupHosts string +param sessionThresholdPerCPU string +param tags object +param timeDifference string +param time string = utcNow('u') +param timestamp string +param timeZone string +param userAssignedIdentityClientId string + +var roleAssignments = [ + resourceGroupControlPlane + resourceGroupHosts +] +var runbookFileName = 'Set-HostPoolScaling.ps1' +var scriptFileName = 'Set-AutomationRunbook.ps1' + +resource automationAccount 'Microsoft.Automation/automationAccounts@2022-08-08' existing = { + name: automationAccountName +} + +module runbook '../common/customScriptExtensions.bicep' = { + name: 'Runbook_${timestamp}' + params: { + fileUris: [ + '${artifactsUri}${runbookFileName}' + '${artifactsUri}${scriptFileName}' + ] + location: location + parameters: '-AutomationAccountName ${automationAccountName} -Environment ${environment().name} -ResourceGroupName ${resourceGroup().name} -RunbookFileName ${runbookFileName} -SubscriptionId ${subscription().subscriptionId} -TenantId ${tenant().tenantId} -UserAssignedIdentityClientId ${userAssignedIdentityClientId}' + scriptFileName: scriptFileName + tags: contains(tags, 'Microsoft.Compute/virtualMachines') ? tags['Microsoft.Compute/virtualMachines'] : {} + userAssignedIdentityClientId: userAssignedIdentityClientId + virtualMachineName: managementVirtualMachineName + } +} + +resource schedules 'Microsoft.Automation/automationAccounts/schedules@2022-08-08' = [for i in range(0, 4): { + parent: automationAccount + name: '${hostPoolName}_${(i + 1) * 15}min' + properties: { + advancedSchedule: {} + description: null + expiryTime: null + frequency: 'Hour' + interval: 1 + startTime: dateTimeAdd(time, 'PT${(i + 1) * 15}M') + timeZone: timeZone + } +}] + +resource jobSchedules 'Microsoft.Automation/automationAccounts/jobSchedules@2022-08-08' = [for i in range(0, 4): { + parent: automationAccount + #disable-next-line use-stable-resource-identifiers + name: guid(time, runbookFileName, hostPoolName, string(i)) + properties: { + parameters: { + beginPeakTime: beginPeakTime + endPeakTime: endPeakTime + EnvironmentName: environment().name + hostPoolName: hostPoolName + limitSecondsToForceLogOffUser: limitSecondsToForceLogOffUser + LogOffMessageBody: 'Your session will be logged off. Please save and close everything.' + LogOffMessageTitle: 'Machine is about to shutdown.' + MaintenanceTagName: 'Maintenance' + minimumNumberOfRdsh: minimumNumberOfRdsh + ResourceGroupName: hostPoolResourceGroupName + sessionThresholdPerCPU: sessionThresholdPerCPU + SubscriptionId: subscription().subscriptionId + TenantId: subscription().tenantId + timeDifference: timeDifference + } + runbook: { + name: replace(runbookFileName, '.ps1', '') + } + runOn: hybridRunbookWorkerGroupName + schedule: { + name: schedules[i].name + } + } + dependsOn: [ + runbook + ] +}] + +// Gives the Automation Account the "Desktop Virtualization Power On Off Contributor" role on the resource groups containing the hosts and host pool +module roleAssignment '../common/roleAssignment.bicep' = [for i in range(0, length(roleAssignments)): { + name: 'RoleAssignment_${i}_${roleAssignments[i]}' + scope: resourceGroup(roleAssignments[i]) + params: { + PrincipalId: automationAccount.identity.principalId + PrincipalType: 'ServicePrincipal' + RoleDefinitionId: '40c5ff49-9181-41f8-ae61-143b0e78555e' // Desktop Virtualization Power On Off Contributor + } +}] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/schedules.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/schedules.bicep new file mode 100644 index 000000000..f4e6f5325 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/schedules.bicep @@ -0,0 +1,37 @@ +param automationAccountName string +param fslogixContainerType string +param storageAccountName string +param time string = utcNow() +param timeZone string + +resource automationAccount 'Microsoft.Automation/automationAccounts@2022-08-08' existing = { + name: automationAccountName +} + +resource schedules_ProfileContainers 'Microsoft.Automation/automationAccounts/schedules@2022-08-08' = [for i in range(0, 4): { + parent: automationAccount + name: '${storageAccountName}_ProfileContainers_${(i + 1) * 15}min' + properties: { + advancedSchedule: {} + description: null + expiryTime: null + frequency: 'Hour' + interval: 1 + startTime: dateTimeAdd(time, 'PT${(i + 1) * 15}M') + timeZone: timeZone + } +}] + +resource schedules_OfficeContainers 'Microsoft.Automation/automationAccounts/schedules@2022-08-08' = [for i in range(0, 4): if (contains(fslogixContainerType, 'Office')) { + parent: automationAccount + name: '${storageAccountName}_OfficeContainers_${(i + 1) * 15}min' + properties: { + advancedSchedule: {} + description: null + expiryTime: null + frequency: 'Hour' + interval: 1 + startTime: dateTimeAdd(time, 'PT${(i + 1) * 15}M') + timeZone: timeZone + } +}] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/userAssignedIdentity.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/userAssignedIdentity.bicep new file mode 100644 index 000000000..032787c90 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/userAssignedIdentity.bicep @@ -0,0 +1,13 @@ +param location string +param name string +param tags object + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { + name: name + location: location + tags: tags +} + +output clientId string = userAssignedIdentity.properties.clientId +output resourceId string = userAssignedIdentity.id +output principalId string = userAssignedIdentity.properties.principalId diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/management/virtualMachine.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/management/virtualMachine.bicep new file mode 100644 index 000000000..ecf2bb036 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/management/virtualMachine.bicep @@ -0,0 +1,256 @@ +param artifactsUri string +param azurePowerShellModuleMsiName string +param deploymentUserAssignedIdentityClientId string +param deploymentUserAssignedIdentityResourceId string +param diskEncryptionSetResourceId string +param diskNamePrefix string +param diskSku string +@secure() +param domainJoinPassword string +param domainJoinUserPrincipalName string +param domainName string +param location string +param networkInterfaceNamePrefix string +param organizationalUnitPath string +param securityLogAnalyticsWorkspaceResourceId string +param subnet string +param tagsNetworkInterfaces object +param tagsVirtualMachines object +param timestamp string = utcNow('yyyyMMddhhmmss') +param virtualNetwork string +param virtualNetworkResourceGroup string +param virtualMachineMonitoringAgent string +param virtualMachineNamePrefix string +@secure() +param virtualMachinePassword string +param virtualMachineUsername string + +var networkInterfaceName = '${networkInterfaceNamePrefix}mgt' +var securitylogAnalyticsWorkspaceName = securityMonitoring ? split(securityLogAnalyticsWorkspaceResourceId, '/')[8] : '' +var securityLogAnalyticsWorkspaceResourceGroupName = securityMonitoring ? split(securityLogAnalyticsWorkspaceResourceId, '/')[4] : resourceGroup().name +var securityLogAnalyticsWorkspaceSubscriptionId = securityMonitoring ? split(securityLogAnalyticsWorkspaceResourceId, '/')[2] : subscription().subscriptionId +var securityMonitoring = empty(securityLogAnalyticsWorkspaceResourceId) ? false : true +var virtualMachineName = '${virtualMachineNamePrefix}mgt' + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = if (securityMonitoring) { + scope: resourceGroup(securityLogAnalyticsWorkspaceSubscriptionId, securityLogAnalyticsWorkspaceResourceGroupName) + name: securitylogAnalyticsWorkspaceName +} + +resource networkInterface 'Microsoft.Network/networkInterfaces@2020-05-01' = { + name: networkInterfaceName + location: location + tags: tagsNetworkInterfaces + properties: { + ipConfigurations: [ + { + name: 'ipconfig' + properties: { + privateIPAllocationMethod: 'Dynamic' + subnet: { + id: resourceId(virtualNetworkResourceGroup, 'Microsoft.Network/virtualNetworks/subnets', virtualNetwork, subnet) + } + primary: true + privateIPAddressVersion: 'IPv4' + } + } + ] + enableAcceleratedNetworking: false + enableIPForwarding: false + } +} + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2021-11-01' = { + name: virtualMachineName + location: location + tags: tagsVirtualMachines + properties: { + hardwareProfile: { + vmSize: 'Standard_B2s' + } + storageProfile: { + imageReference: { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: '2019-datacenter-core-g2' + version: 'latest' + } + osDisk: { + deleteOption: 'Delete' + osType: 'Windows' + createOption: 'FromImage' + caching: 'None' + managedDisk: { + diskEncryptionSet: { + id: diskEncryptionSetResourceId + } + storageAccountType: diskSku + } + name: '${diskNamePrefix}mgt' + } + dataDisks: [] + } + osProfile: { + computerName: virtualMachineName + adminUsername: virtualMachineUsername + adminPassword: virtualMachinePassword + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: false + } + secrets: [] + allowExtensionOperations: true + } + networkProfile: { + networkInterfaces: [ + { + id: networkInterface.id + properties: { + deleteOption: 'Delete' + } + } + ] + } + securityProfile: { + uefiSettings: { + secureBootEnabled: true + vTpmEnabled: true + } + securityType: 'TrustedLaunch' + encryptionAtHost: true + } + diagnosticsProfile: { + bootDiagnostics: { + enabled: false + } + } + licenseType: 'Windows_Server' + } + identity: { + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${deploymentUserAssignedIdentityResourceId}': {} + } + } +} + +resource extension_IaasAntimalware 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = { + parent: virtualMachine + name: 'IaaSAntimalware' + location: location + tags: tagsVirtualMachines + properties: { + publisher: 'Microsoft.Azure.Security' + type: 'IaaSAntimalware' + typeHandlerVersion: '1.3' + autoUpgradeMinorVersion: true + enableAutomaticUpgrade: false + settings: { + AntimalwareEnabled: true + RealtimeProtectionEnabled: 'true' + ScheduledScanSettings: { + isEnabled: 'true' + day: '7' // Day of the week for scheduled scan (1-Sunday, 2-Monday, ..., 7-Saturday) + time: '120' // When to perform the scheduled scan, measured in minutes from midnight (0-1440). For example: 0 = 12AM, 60 = 1AM, 120 = 2AM. + scanType: 'Quick' //Indicates whether scheduled scan setting type is set to Quick or Full (default is Quick) + } + Exclusions: {} + } + } +} + +resource extension_GuestAttestation 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = { + parent: virtualMachine + name: 'GuestAttestation' + location: location + properties: { + publisher: 'Microsoft.Azure.Security.WindowsAttestation' + type: 'GuestAttestation' + typeHandlerVersion: '1.0' + autoUpgradeMinorVersion: true + settings: { + AttestationConfig: { + MaaSettings: { + maaEndpoint: '' + maaTenantName: 'GuestAttestation' + } + AscSettings: { + ascReportingEndpoint: '' + ascReportingFrequency: '' + } + useCustomToken: 'false' + disableAlerts: 'false' + } + } + } +} + +resource extension_MicrosoftMonitoringAgent 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = if (securityMonitoring && virtualMachineMonitoringAgent == 'LogAnalyticsAgent') { + parent: virtualMachine + name: 'MicrosoftmonitoringAgent' + location: location + tags: tagsVirtualMachines + properties: { + publisher: 'Microsoft.EnterpriseCloud.monitoring' + type: 'MicrosoftmonitoringAgent' + typeHandlerVersion: '1.0' + autoUpgradeMinorVersion: true + settings: { + workspaceId: securityMonitoring ? logAnalyticsWorkspace.properties.customerId : null + } + protectedSettings: { + workspaceKey: securityMonitoring ? listKeys(securityLogAnalyticsWorkspaceResourceId, '2021-06-01').primarySharedKey : null + } + } + dependsOn: [ + extension_IaasAntimalware + ] +} + +module extension_CustomScriptExtension '../common/customScriptExtensions.bicep' = { + name: 'CSE_InstallAzurePowerShellAzModule_${timestamp}' + params: { + fileUris: [ + '${artifactsUri}${azurePowerShellModuleMsiName}' + '${artifactsUri}Install-AzurePowerShellAzModule.ps1' + ] + location: location + parameters: '-Installer ${azurePowerShellModuleMsiName}' + scriptFileName: 'Install-AzurePowerShellAzModule.ps1' + tags: tagsVirtualMachines + virtualMachineName: virtualMachine.name + userAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + } + dependsOn: [ + extension_MicrosoftMonitoringAgent + ] +} + +resource extension_JsonADDomainExtension 'Microsoft.Compute/virtualMachines/extensions@2019-07-01' = { + parent: virtualMachine + name: 'JsonADDomainExtension' + location: location + tags: tagsVirtualMachines + properties: { + forceUpdateTag: timestamp + publisher: 'Microsoft.Compute' + type: 'JsonADDomainExtension' + typeHandlerVersion: '1.3' + autoUpgradeMinorVersion: true + settings: { + Name: domainName + Options: '3' + OUPath: organizationalUnitPath + Restart: 'true' + User: domainJoinUserPrincipalName + } + protectedSettings: { + Password: domainJoinPassword + } + } + dependsOn: [ + extension_CustomScriptExtension + ] +} + +output Name string = virtualMachine.name diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/network/networkSecurityGroup.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/network/networkSecurityGroup.bicep new file mode 100644 index 000000000..404ef9c63 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/network/networkSecurityGroup.bicep @@ -0,0 +1,26 @@ +param networkSecurityGroupSecurityRules array +param location string = resourceGroup().location +param networkSecurityGroupName string + +resource networksecuritygroup 'Microsoft.Network/networkSecurityGroups@2020-11-01' = { + name: networkSecurityGroupName + location: location + properties: { + securityRules: [for item in networkSecurityGroupSecurityRules: { + name: item.name + properties: { + access: item.properties.access + destinationAddressPrefix: ((item.properties.destinationAddressPrefix == '') ? null : item.properties.destinationAddressPrefix) + destinationAddressPrefixes: ((length(item.properties.destinationAddressPrefixes) == 0) ? null : item.properties.destinationAddressPrefixes) + destinationPortRanges: ((length(item.properties.destinationPortRanges) == 0) ? null : item.properties.destinationPortRanges) + destinationPortRange: ((item.properties.destinationPortRange == '') ? null : item.properties.destinationPortRange) + direction: item.properties.direction + priority: int(item.properties.priority) + protocol: item.properties.protocol + sourceAddressPrefix: ((item.properties.sourceAddressPrefix == '') ? null : item.properties.sourceAddressPrefix) + sourcePortRanges: ((length(item.properties.sourcePortRanges) == 0) ? null : item.properties.sourcePortRanges) + sourcePortRange: item.properties.sourcePortRange + } + }] + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/network/networking.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/network/networking.bicep new file mode 100644 index 000000000..8c35b87cb --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/network/networking.bicep @@ -0,0 +1,123 @@ +targetScope = 'subscription' + +param azureNetAppFilesSubnetAddressPrefix string +param disableBgpRoutePropagation bool +param hubAzureFirewallResourceId string +param hubVirtualNetworkResourceId string +param index int +param location string +param networkSecurityGroupName string +param subnetAddressPrefixes array +param resourceGroupNetwork string +param routeTableName string +param timestamp string +param virtualNetworkAddressPrefixes array +param virtualNetworkName string + +var hubSubscriptionId = split(hubVirtualNetworkResourceId, '/')[2] +var hubVirtualNetworkName = split(hubVirtualNetworkResourceId, '/')[8] +var hubVirtualNetworkResourceGroupName = split(hubVirtualNetworkResourceId, '/')[4] +var networkSecurityGroupSecurityRules = [] +var spokeResourceGroup = resourceGroupNetwork +var spokeSubscriptionId = subscription().subscriptionId +var subnets = union(subnetWorkload, subnetAnf) +var subnetAnf = empty(azureNetAppFilesSubnetAddressPrefix) ? [] : [ + { + name: 'AzureNetAppFiles' + addressPrefix: azureNetAppFilesSubnetAddressPrefix + delegations: [ + { + name: 'Microsoft.Netapp.volumes' + id: '${resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, 'AzureNetAppFiles')}/delegations/Microsoft.Netapp.volumes' + properties: { + serviceName: 'Microsoft.Netapp/volumes' + } + type: 'Microsoft.Network/virtualNetworks/subnets/delegations' + } + ] + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Disabled' + networkSecurityGroupName: networkSecurityGroupName + } +] +var subnetWorkload = [ + { + name: 'AzureVirtualDesktop' + addressPrefix: subnetAddressPrefixes[index] + delegations: [] + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Disabled' + networkSecurityGroupName: networkSecurityGroupName + } +] + +resource hubVirtualNetwork 'Microsoft.Network/virtualNetworks@2023-05-01' existing = { + scope: resourceGroup(hubSubscriptionId, hubVirtualNetworkResourceGroupName) + name: hubVirtualNetworkName +} + +resource azureFirewall 'Microsoft.Network/azureFirewalls@2023-05-01' existing = { + scope: resourceGroup(split(hubAzureFirewallResourceId, '/')[2], split(hubAzureFirewallResourceId, '/')[4]) + name: split(hubAzureFirewallResourceId, '/')[8] +} + +module userDefinedRoute 'userDefinedRoute.bicep' = { + name: 'UserDefinedRoute_${index}_${timestamp}' + scope: resourceGroup(spokeSubscriptionId, spokeResourceGroup) + params: { + azureFirewallIpAddress: azureFirewall.properties.ipConfigurations[0].properties.privateIPAddress + disableBgpRoutePropagation: disableBgpRoutePropagation + location: location + udrName: routeTableName + } +} + +module networkSecurityGroup 'networkSecurityGroup.bicep' = { + name: 'NetworkSecurityGroup_${index}_${timestamp}' + scope: resourceGroup(spokeSubscriptionId, spokeResourceGroup) + params: { + location: location + networkSecurityGroupName: networkSecurityGroupName + networkSecurityGroupSecurityRules: networkSecurityGroupSecurityRules + } +} + +module spokeVirtualNetwork 'virtualNetwork.bicep' = { + name: 'VirtualNetwork_${index}_${timestamp}' + scope: resourceGroup(spokeSubscriptionId, spokeResourceGroup) + params: { + dnsServers: hubVirtualNetwork.properties.dhcpOptions.dnsServers + location: location + subnets: subnets + udrName: userDefinedRoute.outputs.name + virtualNetworkName: virtualNetworkName + vNetAddressPrefixes: [ + virtualNetworkAddressPrefixes[index] + ] + } +} + +module virtualNetworkPeeringToHub 'virtualNetworkPeering.bicep' = { + name: 'VirtualNetworkPeer_Hub_${index}_${timestamp}' + scope: resourceGroup(spokeSubscriptionId, spokeResourceGroup) + params: { + existingLocalVirtualNetworkName: spokeVirtualNetwork.outputs.virtualNetworkName + existingRemoteVirtualNetworkName: hubVirtualNetwork.name + existingRemoteVirtualNetworkResourceGroupName: hubVirtualNetworkResourceGroupName + } +} + +module virtualNetworkPeeringToSpoke 'virtualNetworkPeering.bicep' = { + name: 'VirtualNetworkPeer_Spoke_${index}_${timestamp}' + scope: resourceGroup(hubSubscriptionId, hubVirtualNetworkResourceGroupName) + params: { + existingLocalVirtualNetworkName: hubVirtualNetwork.name + existingRemoteVirtualNetworkName: spokeVirtualNetwork.outputs.virtualNetworkName + existingRemoteVirtualNetworkResourceGroupName: spokeResourceGroup + } + dependsOn: [ + virtualNetworkPeeringToHub + ] +} + +output subnetResourceId string = spokeVirtualNetwork.outputs.subnetResourceId diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/network/userDefinedRoute.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/network/userDefinedRoute.bicep new file mode 100644 index 000000000..35428aae5 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/network/userDefinedRoute.bicep @@ -0,0 +1,26 @@ +param azureFirewallIpAddress string +param disableBgpRoutePropagation bool +param location string +param udrName string + +resource routeTable 'Microsoft.Network/routeTables@2021-05-01' = { + name: udrName + location: location + properties: { + disableBgpRoutePropagation: disableBgpRoutePropagation + routes: [ + { + name: 'default' + properties: { + addressPrefix: '0.0.0.0/0' + hasBgpOverride: false + nextHopIpAddress: azureFirewallIpAddress + nextHopType: 'VirtualAppliance' + } + } + ] + } +} + +output name string = routeTable.name +output id string = routeTable.id diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/network/virtualNetwork.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/network/virtualNetwork.bicep new file mode 100644 index 000000000..6f894a3e0 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/network/virtualNetwork.bicep @@ -0,0 +1,41 @@ +param dnsServers array +param location string = resourceGroup().location +param virtualNetworkName string +param subnets array +param udrName string +param vNetAddressPrefixes array + +resource userDefinedRoute 'Microsoft.Network/routeTables@2021-05-01' existing = { + name: udrName +} + +resource virtualNetwork 'Microsoft.Network/virtualNetworks@2020-11-01' = { + name: virtualNetworkName + location: location + properties: { + addressSpace: { + addressPrefixes: vNetAddressPrefixes + } + dhcpOptions: { + dnsServers: dnsServers + } + subnets: [for item in subnets: { + name: item.name + properties: { + addressPrefix: item.addressPrefix + delegations: item.delegations + networkSecurityGroup: (empty(item.networkSecurityGroupName) ? null : json('{"id": "${resourceId('Microsoft.Network/networkSecurityGroups', item.networkSecurityGroupName)}"}')) + privateEndpointNetworkPolicies: item.privateEndpointNetworkPolicies + privateLinkServiceNetworkPolicies: item.privateLinkServiceNetworkPolicies + routeTable: { + id: userDefinedRoute.id + } + + } + }] + } +} + +output virtualNetworkName string = virtualNetwork.name +output virtualNetworkResourceId string = virtualNetwork.id +output subnetResourceId string = virtualNetwork.properties.subnets[0].id diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/network/virtualNetworkPeering.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/network/virtualNetworkPeering.bicep new file mode 100644 index 000000000..0f7e20b7c --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/network/virtualNetworkPeering.bicep @@ -0,0 +1,16 @@ +param existingLocalVirtualNetworkName string +param existingRemoteVirtualNetworkName string +param existingRemoteVirtualNetworkResourceGroupName string + +resource existingLocalVirtualNetworkName_peering_to_remote_vnet 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2021-02-01' = { + name: '${existingLocalVirtualNetworkName}/${existingRemoteVirtualNetworkName}' + properties: { + allowVirtualNetworkAccess: true + allowForwardedTraffic: true + allowGatewayTransit: false + useRemoteGateways: false + remoteVirtualNetwork: { + id: resourceId(existingRemoteVirtualNetworkResourceGroupName, 'Microsoft.Network/virtualNetworks', existingRemoteVirtualNetworkName) + } + } +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/resourceGroup.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/resourceGroup.bicep new file mode 100644 index 000000000..e689bc4f0 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/resourceGroup.bicep @@ -0,0 +1,11 @@ +targetScope = 'subscription' + +param location string +param resourceGroupName string +param tags object + +resource resourceGroup 'Microsoft.Resources/resourceGroups@2020-10-01' = { + name: resourceGroupName + location: location + tags: contains(tags, 'Microsoft.Resources/resourceGroups') ? tags['Microsoft.Resources/resourceGroups'] : {} +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/resourceNames.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/resourceNames.bicep new file mode 100644 index 000000000..66fc69895 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/resourceNames.bicep @@ -0,0 +1,154 @@ +targetScope = 'subscription' + +param environmentShortName string +param identifier string +param locationControlPlane string +param locationVirtualMachines string +param stampIndex int + +// NAMING CONVENTIONS +// All the resources are named using the following variables +// Modify the components of the naming convention to suit your needs +var namingConvention = '${identifier}-${stampIndex}-resourceType-${environmentShortName}-location' +var namingConvention_Global = 'resourceType-${environmentShortName}-location' +var namingConvention_Shared = '${identifier}-resourceType-${environmentShortName}-location' + +// SUPPORTING DATA +var cloudEndpointSuffix = replace(replace(environment().resourceManager, 'https://management.', ''), '/', '') +var privateDnsZoneSuffixes_AzureAutomation = { + AzureCloud: 'net' + AzureUSGovernment: 'us' +} +var privateDnsZoneSuffixes_AzureVirtualDesktop = { + AzureCloud: 'microsoft.com' + AzureUSGovernment: 'azure.us' +} +var privateDnsZoneSuffixes_Backup = { + AzureCloud: 'windowsazure.com' + AzureUSGovernment: 'windowsazure.us' +} +var privateDnsZoneSuffixes_Monitor = { + AzureCloud: 'azure.com' + AzureUSGovernment: 'azure.us' +} +var locations = (loadJsonContent('../data/locations.json'))[environment().name] +var resourceAbbreviations = loadJsonContent('../data/resourceAbbreviations.json') + +// RESOURCE NAMES AND PREFIXES + +var agentSvcPrivateDnsZoneName = 'privatelink.agentsvc.azure-automation.${privateDnsZoneSuffixes_AzureAutomation[environment().name] ?? cloudEndpointSuffix}' +var automationAccountName = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.automationAccounts), 'location', locations[locationVirtualMachines].abbreviation) +var availabilitySetNamePrefix = '${replace(replace(namingConvention, 'resourceType', resourceAbbreviations.availabilitySets), 'location', locations[locationVirtualMachines].abbreviation)}-' +var avdGlobalPrivateDnsZoneName = 'privatelink-global.wvd.${privateDnsZoneSuffixes_AzureVirtualDesktop[environment().name] ?? cloudEndpointSuffix}' +var avdPrivateDnsZoneName = 'privatelink.wvd.${privateDnsZoneSuffixes_AzureVirtualDesktop[environment().name] ?? cloudEndpointSuffix}' +var azureAutomationPrivateDnsZoneName = 'privatelink.azure-automation.${privateDnsZoneSuffixes_AzureAutomation[environment().name] ?? cloudEndpointSuffix}' +var backupPrivateDnsZoneName = 'privatelink.${locations[locationVirtualMachines].recoveryServicesGeo}.backup.${privateDnsZoneSuffixes_Backup[environment().name] ?? cloudEndpointSuffix}' +var blobPrivateDnsZoneName = 'privatelink.blob.${environment().suffixes.storage}' +var dataCollectionRuleAssociationName = '${replace(replace(namingConvention, 'resourceType', resourceAbbreviations.dataCollectionRuleAssociations), 'location', locations[locationVirtualMachines].abbreviation)}-avdi' +var dataCollectionRuleName = 'microsoft-avdi-${locations[locationVirtualMachines].abbreviation}' +var desktopApplicationGroupName = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.desktopApplicationGroups), 'location', locations[locationControlPlane].abbreviation) +var diskAccessName = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.diskAccesses), 'location', locations[locationVirtualMachines].abbreviation) +var diskEncryptionSetName = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.diskEncryptionSets), 'location', locations[locationVirtualMachines].abbreviation) +var diskNamePrefix = '${replace(replace(namingConvention, 'resourceType', resourceAbbreviations.disks), 'location', locations[locationVirtualMachines].abbreviation)}-' +var filePrivateDnsZoneName = 'privatelink.file.${environment().suffixes.storage}' +var fileShareNames = { + CloudCacheProfileContainer: [ + 'profile-containers' + ] + CloudCacheProfileOfficeContainer: [ + 'office-containers' + 'profile-containers' + ] + ProfileContainer: [ + 'profile-containers' + ] + ProfileOfficeContainer: [ + 'office-containers' + 'profile-containers' + ] +} +var hostPoolName = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.hostPools), 'location', locations[locationControlPlane].abbreviation) +var keyVaultName = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.keyVaults), 'location', locations[locationVirtualMachines].abbreviation) +var keyVaultPrivateDnsZoneName = replace('privatelink${environment().suffixes.keyvaultDns}', 'vault', 'vaultcore') +var logAnalyticsWorkspaceName = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.logAnalyticsWorkspaces), 'location', locations[locationVirtualMachines].abbreviation) +var netAppAccountName = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.netAppAccounts), 'location', locations[locationVirtualMachines].abbreviation) +var netAppCapacityPoolName = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.netAppCapacityPools), 'location', locations[locationVirtualMachines].abbreviation) +var networkInterfaceNamePrefix = '${replace(replace(namingConvention, 'resourceType', resourceAbbreviations.networkInterfaces), 'location', locations[locationVirtualMachines].abbreviation)}-' +var networkSecurityGroupNames = [ + replace(replace(namingConvention, 'resourceType', resourceAbbreviations.networkSecurityGroups), 'location', locations[locationControlPlane].abbreviation) + replace(replace(namingConvention, 'resourceType', resourceAbbreviations.networkSecurityGroups), 'location', locations[locationVirtualMachines].abbreviation) +] +var monitorPrivateDnsZoneName = 'privatelink.monitor.${privateDnsZoneSuffixes_Monitor[environment().name] ?? cloudEndpointSuffix}' +var odsOpinsightsPrivateDnsZoneName = 'privatelink.ods.opinsights.${privateDnsZoneSuffixes_Monitor[environment().name] ?? cloudEndpointSuffix}' +var omsOpinsightsPrivateDnsZoneName = 'privatelink.oms.opinsights.${privateDnsZoneSuffixes_Monitor[environment().name] ?? cloudEndpointSuffix}' +var queuePrivateDnsZoneName = 'privatelink.queue.${environment().suffixes.storage}' +var recoveryServicesVaultName = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.recoveryServicesVaults), 'location', locations[locationVirtualMachines].abbreviation) +var resourceGroupControlPlane = '${replace(replace(namingConvention, 'resourceType', resourceAbbreviations.resourceGroups), 'location', locations[locationControlPlane].abbreviation)}-avd-controlPlane' +var resourceGroupFeedWorkspace = '${replace(replace(namingConvention_Shared, 'resourceType', resourceAbbreviations.resourceGroups), 'location', locations[locationControlPlane].abbreviation)}-avd-feedWorkspace' +var resourceGroupGlobalWorkspace = '${replace(replace(namingConvention_Global, 'resourceType', resourceAbbreviations.resourceGroups), 'location', locations[locationControlPlane].abbreviation)}-avd-globalWorkspace' +var resourceGroupHosts = '${replace(replace(namingConvention, 'resourceType', resourceAbbreviations.resourceGroups), 'location', locations[locationVirtualMachines].abbreviation)}-avd-sessionHosts' +var resourceGroupManagement = '${replace(replace(namingConvention, 'resourceType', resourceAbbreviations.resourceGroups), 'location', locations[locationVirtualMachines].abbreviation)}-avd-management' +var resourceGroupsNetwork = [ + '${replace(replace(namingConvention, 'resourceType', resourceAbbreviations.resourceGroups), 'location', locations[locationControlPlane].abbreviation)}-avd-network' + '${replace(replace(namingConvention, 'resourceType', resourceAbbreviations.resourceGroups), 'location', locations[locationVirtualMachines].abbreviation)}-avd-network' +] +var resourceGroupStorage = '${replace(replace(namingConvention, 'resourceType', resourceAbbreviations.resourceGroups), 'location', locations[locationVirtualMachines].abbreviation)}-avd-profileStorage' +var routeTables = [ + replace(replace(namingConvention, 'resourceType', resourceAbbreviations.routeTables), 'location', locations[locationControlPlane].abbreviation) + replace(replace(namingConvention, 'resourceType', resourceAbbreviations.routeTables), 'location', locations[locationVirtualMachines].abbreviation) +] +var storageAccountNamePrefix = replace(replace(replace(replace(namingConvention, 'resourceType', resourceAbbreviations.storageAccounts), 'location', locations[locationVirtualMachines].abbreviation), environmentShortName, first(environmentShortName)), '-', '') +var userAssignedIdentityNamePrefix = replace(replace(namingConvention, 'resourceType', resourceAbbreviations.userAssignedIdentities), 'location', locations[locationVirtualMachines].abbreviation) +var virtualMachineNamePrefix = replace(replace(replace(replace(namingConvention, 'resourceType', resourceAbbreviations.virtualMachines), 'location', locations[locationVirtualMachines].abbreviation), environmentShortName, first(environmentShortName)), '-', '') +var virtualNetworkNames = [ + replace(replace(namingConvention, 'resourceType', resourceAbbreviations.virtualNetworks), 'location', locations[locationControlPlane].abbreviation) + replace(replace(namingConvention, 'resourceType', resourceAbbreviations.virtualNetworks), 'location', locations[locationVirtualMachines].abbreviation) +] +var workspaceFeedNamePrefix = replace(replace(namingConvention_Shared, 'resourceType', resourceAbbreviations.workspaces), 'location', locations[locationControlPlane].abbreviation) +var workspaceGlobalNamePrefix = replace(replace(namingConvention_Global, 'resourceType', resourceAbbreviations.workspaces), 'location', locations[locationControlPlane].abbreviation) + +output agentSvcPrivateDnsZoneName string = agentSvcPrivateDnsZoneName +output automationAccountName string = automationAccountName +output availabilitySetNamePrefix string = availabilitySetNamePrefix +output avdGlobalPrivateDnsZoneName string = avdGlobalPrivateDnsZoneName +output avdPrivateDnsZoneName string = avdPrivateDnsZoneName +output azureAutomationPrivateDnsZoneName string = azureAutomationPrivateDnsZoneName +output backupPrivateDnsZoneName string = backupPrivateDnsZoneName +output blobPrivateDnsZoneName string = blobPrivateDnsZoneName +output dataCollectionRuleAssociationName string = dataCollectionRuleAssociationName +output dataCollectionRuleName string = dataCollectionRuleName +output desktopApplicationGroupName string = desktopApplicationGroupName +output diskAccessName string = diskAccessName +output diskEncryptionSetName string = diskEncryptionSetName +output diskNamePrefix string = diskNamePrefix +output filePrivateDnsZoneName string = filePrivateDnsZoneName +output fileShareNames object = fileShareNames +output hostPoolName string = hostPoolName +output keyVaultName string = keyVaultName +output keyVaultPrivateDnsZoneName string = keyVaultPrivateDnsZoneName +output locations object = locations +output logAnalyticsWorkspaceName string = logAnalyticsWorkspaceName +output monitorPrivateDnsZoneName string = monitorPrivateDnsZoneName +output odsOpinsightsPrivateDnsZoneName string = odsOpinsightsPrivateDnsZoneName +output omsOpinsightsPrivateDnsZoneName string = omsOpinsightsPrivateDnsZoneName +output netAppAccountName string = netAppAccountName +output netAppCapacityPoolName string = netAppCapacityPoolName +output networkInterfaceNamePrefix string = networkInterfaceNamePrefix +output networkSecurityGroupNames array = networkSecurityGroupNames +output queuePrivateDnsZoneName string = queuePrivateDnsZoneName +output recoveryServicesVaultName string = recoveryServicesVaultName +output resourceAbbreviations object = resourceAbbreviations +output resourceGroupControlPlane string = resourceGroupControlPlane +output resourceGroupFeedWorkspace string = resourceGroupFeedWorkspace +output resourceGroupGlobalWorkspace string = resourceGroupGlobalWorkspace +output resourceGroupHosts string = resourceGroupHosts +output resourceGroupManagement string = resourceGroupManagement +output resourceGroupsNetwork array = resourceGroupsNetwork +output resourceGroupStorage string = resourceGroupStorage +output routeTables array = routeTables +output storageAccountNamePrefix string = storageAccountNamePrefix +output userAssignedIdentityNamePrefix string = userAssignedIdentityNamePrefix +output virtualMachineNamePrefix string = virtualMachineNamePrefix +output virtulNetworkNames array = virtualNetworkNames +output workspaceFeedNamePrefix string = workspaceFeedNamePrefix +output workspaceGlobalNamePrefix string = workspaceGlobalNamePrefix diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/availabilitySets.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/availabilitySets.bicep new file mode 100644 index 000000000..9098817d1 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/availabilitySets.bicep @@ -0,0 +1,18 @@ +param availabilitySetNamePrefix string +param availabilitySetsCount int +param availabilitySetsIndex int +param location string +param tagsAvailabilitySets object + +resource availabilitySets 'Microsoft.Compute/availabilitySets@2019-07-01' = [for i in range(0, availabilitySetsCount): { + name: '${availabilitySetNamePrefix}${padLeft((i + availabilitySetsIndex), 2, '0')}' + location: location + tags: tagsAvailabilitySets + sku: { + name: 'Aligned' + } + properties: { + platformUpdateDomainCount: 5 + platformFaultDomainCount: 2 + } +}] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/protectedItems.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/protectedItems.bicep new file mode 100644 index 000000000..76f94fb47 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/protectedItems.bicep @@ -0,0 +1,22 @@ +param location string +param policyId string +param recoveryServicesVaultName string +param sessionHostCount int +param sessionHostIndex int +param tags object +param virtualMachineNamePrefix string +param virtualMachineResourceGroupName string + +var v2VmContainer = 'iaasvmcontainer;iaasvmcontainerv2;' +var v2Vm = 'vm;iaasvmcontainerv2;' + +resource protectedItems_Vm 'Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems@2021-08-01' = [for i in range(0, sessionHostCount): { + name: '${recoveryServicesVaultName}/Azure/${v2VmContainer}${virtualMachineResourceGroupName};${virtualMachineNamePrefix}${padLeft((i + sessionHostIndex), 4, '0')}/${v2Vm}${virtualMachineResourceGroupName};${virtualMachineNamePrefix}${padLeft((i + sessionHostIndex), 4, '0')}' + location: location + tags: tags + properties: { + protectedItemType: 'Microsoft.Compute/virtualMachines' + policyId: policyId + sourceResourceId: resourceId(virtualMachineResourceGroupName, 'Microsoft.Compute/virtualMachines', '${virtualMachineNamePrefix}${padLeft((i + sessionHostIndex), 4, '0')}') + } +}] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/recoveryServices.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/recoveryServices.bicep new file mode 100644 index 000000000..b4dd9fa80 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/recoveryServices.bicep @@ -0,0 +1,37 @@ +param divisionRemainderValue int +param fslogix bool +param location string +param maxResourcesPerTemplateDeployment int +param recoveryServicesVaultName string +param resourceGroupHosts string +param resourceGroupManagement string +param sessionHostBatchCount int +param sessionHostIndex int +param tagsRecoveryServicesVault object +param timestamp string +param virtualMachineNamePrefix string + +resource vault 'Microsoft.RecoveryServices/vaults@2022-03-01' existing = { + name: recoveryServicesVaultName + scope: resourceGroup(resourceGroupManagement) +} + +resource backupPolicy_Vm 'Microsoft.RecoveryServices/vaults/backupPolicies@2022-03-01' existing = { + parent: vault + name: 'AvdPolicyVm' +} + +module protectedItems_Vm 'protectedItems.bicep' = [for i in range(1, sessionHostBatchCount): if (!fslogix) { + name: 'BackupProtectedItems_VirtualMachines_${i - 1}_${timestamp}' + scope: resourceGroup(resourceGroupManagement) // Management Resource Group + params: { + location: location + policyId: backupPolicy_Vm.id + recoveryServicesVaultName: vault.name + sessionHostCount: i == sessionHostBatchCount && divisionRemainderValue > 0 ? divisionRemainderValue : maxResourcesPerTemplateDeployment + sessionHostIndex: i == 1 ? sessionHostIndex : ((i - 1) * maxResourcesPerTemplateDeployment) + sessionHostIndex + tags: tagsRecoveryServicesVault + virtualMachineNamePrefix: virtualMachineNamePrefix + virtualMachineResourceGroupName: resourceGroupHosts + } +}] diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/sessionHosts.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/sessionHosts.bicep new file mode 100644 index 000000000..d70e547bb --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/sessionHosts.bicep @@ -0,0 +1,228 @@ +targetScope = 'subscription' + +param acceleratedNetworking string +param activeDirectorySolution string +param artifactsUri string +param artifactsUserAssignedIdentityClientId string +param artifactsUserAssignedIdentityResourceId string +param automationAccountName string +param availability string +param availabilitySetNamePrefix string +param availabilitySetsCount int +param availabilitySetsIndex int +param availabilityZones array +param avdAgentBootLoaderMsiName string +param avdAgentMsiName string +param dataCollectionRuleAssociationName string +param dataCollectionRuleResourceId string +param deploymentUserAssignedIdentityClientId string +param diskEncryptionSetResourceId string +param diskNamePrefix string +param diskSku string +param divisionRemainderValue int +@secure() +param domainJoinPassword string +param domainJoinUserPrincipalName string +param domainName string +param drainMode bool +param fslogix bool +param fslogixContainerType string +param hostPoolName string +param hostPoolType string +param hybridRunbookWorkerGroupName string +param imageDefinitionResourceId string +param imageOffer string +param imagePublisher string +param imageSku string +param location string +param logAnalyticsWorkspaceName string +param managementVirtualMachineName string +param maxResourcesPerTemplateDeployment int +param monitoring bool +param netAppFileShares array +param networkInterfaceNamePrefix string +param organizationalUnitPath string +param pooledHostPool bool +param enableRecoveryServices bool +param enableScalingTool bool +param recoveryServicesVaultName string +param resourceGroupControlPlane string +param resourceGroupHosts string +param resourceGroupManagement string +param roleDefinitions object +param scalingBeginPeakTime string +param scalingEndPeakTime string +param scalingLimitSecondsToForceLogOffUser string +param scalingMinimumNumberOfRdsh string +param scalingSessionThresholdPerCPU string +param securityPrincipalObjectIds array +param securityLogAnalyticsWorkspaceResourceId string +param sessionHostBatchCount int +param sessionHostIndex int +param storageAccountPrefix string +param storageCount int +param storageIndex int +param storageService string +param storageSuffix string +param subnet string +param tags object +param timeDifference string +param timestamp string +param timeZone string +param virtualMachineMonitoringAgent string +param virtualMachineNamePrefix string +@secure() +param virtualMachinePassword string +param virtualMachineSize string +param virtualMachineUsername string +param virtualNetwork string +param virtualNetworkResourceGroup string + +var tagsAutomationAccounts = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupManagement}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.Automation/automationAccounts') ? tags['Microsoft.Automation/automationAccounts'] : {}) +var tagsAvailabilitySets = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupManagement}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.Compute/availabilitySets') ? tags['Microsoft.Compute/availabilitySets'] : {}) +var tagsNetworkInterfaces = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupManagement}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.Network/networkInterfaces') ? tags['Microsoft.Network/networkInterfaces'] : {}) +var tagsRecoveryServicesVault = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupManagement}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.recoveryServices/vaults') ? tags['Microsoft.recoveryServices/vaults'] : {}) +var tagsVirtualMachines = union({'cm-resource-parent': '${subscription().id}}/resourceGroups/${resourceGroupManagement}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}'}, contains(tags, 'Microsoft.Compute/virtualMachines') ? tags['Microsoft.Compute/virtualMachines'] : {}) + +module availabilitySets 'availabilitySets.bicep' = if (pooledHostPool && availability == 'availabilitySets') { + name: 'availabilitySets_${timestamp}' + scope: resourceGroup(resourceGroupHosts) + params: { + availabilitySetNamePrefix: availabilitySetNamePrefix + availabilitySetsCount: availabilitySetsCount + availabilitySetsIndex: availabilitySetsIndex + location: location + tagsAvailabilitySets: tagsAvailabilitySets + } +} + +// Role Assignment for Virtual Machine Login User +// This module deploys the role assignments to login to Azure AD joined session hosts +module roleAssignments '../common/roleAssignment.bicep' = [for i in range(0, length(securityPrincipalObjectIds)): if (!contains(activeDirectorySolution, 'DomainServices')) { + name: 'RoleAssignments_${i}_${timestamp}' + scope: resourceGroup(resourceGroupHosts) + params: { + PrincipalId: securityPrincipalObjectIds[i] + PrincipalType: 'Group' + RoleDefinitionId: roleDefinitions.VirtualMachineUserLogin + } +}] + +@batchSize(1) +module virtualMachines 'virtualMachines.bicep' = [for i in range(1, sessionHostBatchCount): { + name: 'VirtualMachines_${i - 1}_${timestamp}' + scope: resourceGroup(resourceGroupHosts) + params: { + acceleratedNetworking: acceleratedNetworking + activeDirectorySolution: activeDirectorySolution + artifactsUri: artifactsUri + artifactsUserAssignedIdentityClientId: artifactsUserAssignedIdentityClientId + artifactsUserAssignedIdentityResourceId: artifactsUserAssignedIdentityResourceId + availability: availability + availabilitySetNamePrefix: availabilitySetNamePrefix + availabilityZones: availabilityZones + avdAgentBootLoaderMsiName: avdAgentBootLoaderMsiName + avdAgentMsiName: avdAgentMsiName + batchCount: i + dataCollectionRuleAssociationName: dataCollectionRuleAssociationName + dataCollectionRuleResourceId: dataCollectionRuleResourceId + deploymentUserAssignedidentityClientId: deploymentUserAssignedIdentityClientId + diskEncryptionSetResourceId: diskEncryptionSetResourceId + diskNamePrefix: diskNamePrefix + diskSku: diskSku + domainJoinPassword: domainJoinPassword + domainJoinUserPrincipalName: domainJoinUserPrincipalName + domainName: domainName + enableDrainMode: drainMode + fslogix: fslogix + fslogixContainerType: fslogixContainerType + hostPoolName: hostPoolName + hostPoolType: hostPoolType + imageDefinitionResourceId: imageDefinitionResourceId + imageOffer: imageOffer + imagePublisher: imagePublisher + imageSku: imageSku + location: location + logAnalyticsWorkspaceName: logAnalyticsWorkspaceName + managementVirtualMachineName: managementVirtualMachineName + monitoring: monitoring + netAppFileShares: netAppFileShares + networkInterfaceNamePrefix: networkInterfaceNamePrefix + organizationalUnitPath: organizationalUnitPath + resourceGroupControlPlane: resourceGroupControlPlane + resourceGroupManagement: resourceGroupManagement + securityLogAnalyticsWorkspaceResourceId: securityLogAnalyticsWorkspaceResourceId + sessionHostCount: i == sessionHostBatchCount && divisionRemainderValue > 0 ? divisionRemainderValue : maxResourcesPerTemplateDeployment + sessionHostIndex: i == 1 ? sessionHostIndex : ((i - 1) * maxResourcesPerTemplateDeployment) + sessionHostIndex + storageAccountPrefix: storageAccountPrefix + storageCount: storageCount + storageIndex: storageIndex + storageService: storageService + storageSuffix: storageSuffix + subnet: subnet + tagsNetworkInterfaces: tagsNetworkInterfaces + tagsVirtualMachines: tagsVirtualMachines + timestamp: timestamp + virtualMachineMonitoringAgent: virtualMachineMonitoringAgent + virtualMachineNamePrefix: virtualMachineNamePrefix + virtualMachinePassword: virtualMachinePassword + virtualMachineSize: virtualMachineSize + virtualMachineUsername: virtualMachineUsername + virtualNetwork: virtualNetwork + virtualNetworkResourceGroup: virtualNetworkResourceGroup + } + dependsOn: [ + availabilitySets + ] +}] + +module recoveryServices 'recoveryServices.bicep' = if (enableRecoveryServices && contains(hostPoolType, 'Personal')) { + name: 'RecoveryServices_VirtualMachines_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + divisionRemainderValue: divisionRemainderValue + fslogix: fslogix + location: location + maxResourcesPerTemplateDeployment: maxResourcesPerTemplateDeployment + recoveryServicesVaultName: recoveryServicesVaultName + resourceGroupHosts: resourceGroupHosts + resourceGroupManagement: resourceGroupManagement + sessionHostBatchCount: sessionHostBatchCount + sessionHostIndex: sessionHostIndex + tagsRecoveryServicesVault: tagsRecoveryServicesVault + timestamp: timestamp + virtualMachineNamePrefix: virtualMachineNamePrefix + } + dependsOn: [ + virtualMachines + ] +} + +module scalingTool '../management/scalingTool.bicep' = if (enableScalingTool && pooledHostPool) { + name: 'ScalingTool_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + artifactsUri: artifactsUri + automationAccountName: automationAccountName + beginPeakTime: scalingBeginPeakTime + endPeakTime: scalingEndPeakTime + hostPoolName: hostPoolName + hostPoolResourceGroupName: resourceGroupControlPlane + hybridRunbookWorkerGroupName: hybridRunbookWorkerGroupName + limitSecondsToForceLogOffUser: scalingLimitSecondsToForceLogOffUser + location: location + managementVirtualMachineName: managementVirtualMachineName + minimumNumberOfRdsh: scalingMinimumNumberOfRdsh + resourceGroupControlPlane: resourceGroupControlPlane + resourceGroupHosts: resourceGroupHosts + sessionThresholdPerCPU: scalingSessionThresholdPerCPU + tags: tagsAutomationAccounts + timeDifference: timeDifference + timestamp: timestamp + timeZone: timeZone + userAssignedIdentityClientId: deploymentUserAssignedIdentityClientId + } + dependsOn: [ + recoveryServices + ] +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/virtualMachines.bicep b/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/virtualMachines.bicep new file mode 100644 index 000000000..e6955f4f5 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/modules/sessionHosts/virtualMachines.bicep @@ -0,0 +1,447 @@ +param artifactsUri string +param artifactsUserAssignedIdentityClientId string +param artifactsUserAssignedIdentityResourceId string +param acceleratedNetworking string +param activeDirectorySolution string +param availability string +param availabilitySetNamePrefix string +param availabilityZones array +param avdAgentBootLoaderMsiName string +param avdAgentMsiName string +param batchCount int +param dataCollectionRuleAssociationName string +param dataCollectionRuleResourceId string +param deploymentUserAssignedidentityClientId string +param diskEncryptionSetResourceId string +param diskNamePrefix string +param diskSku string +@secure() +param domainJoinPassword string +param domainJoinUserPrincipalName string +param domainName string +param enableDrainMode bool +param fslogix bool +param fslogixContainerType string +param hostPoolName string +param hostPoolType string +param imageDefinitionResourceId string +param imageOffer string +param imagePublisher string +param imageSku string +param location string +param logAnalyticsWorkspaceName string +param managementVirtualMachineName string +param monitoring bool +param netAppFileShares array +param networkInterfaceNamePrefix string +param organizationalUnitPath string +param resourceGroupControlPlane string +param resourceGroupManagement string +param securityLogAnalyticsWorkspaceResourceId string +param sessionHostCount int +param sessionHostIndex int +param storageAccountPrefix string +param storageCount int +param storageIndex int +param storageService string +param storageSuffix string +param subnet string +param tagsNetworkInterfaces object +param tagsVirtualMachines object +param timestamp string +param virtualMachineMonitoringAgent string +param virtualMachineNamePrefix string +@secure() +param virtualMachinePassword string +param virtualMachineSize string +param virtualMachineUsername string +param virtualNetwork string +param virtualNetworkResourceGroup string + +var amdVmSize = contains(amdVmSizes, virtualMachineSize) +var amdVmSizes = [ + 'Standard_NV4as_v4' + 'Standard_NV8as_v4' + 'Standard_NV16as_v4' + 'Standard_NV32as_v4' +] +var fslogixExclusions = '"%TEMP%\\*\\*.VHDX";"%Windir%\\TEMP\\*\\*.VHDX"${fslogixExclusionsCloudCache}${fslogixExclusionsProfileContainers}${fslogixExclusionsOfficeContainers}' +var fslogixExclusionsCloudCache = contains(fslogixContainerType, 'CloudCache') ? ';"%ProgramData%\\fslogix\\Cache\\*";"%ProgramData%\\fslogix\\Proxy\\*"' : '' +var fslogixExclusionsOfficeContainers = contains(fslogixContainerType, 'Office') ? ';"${fslogixOfficeShare}";"${fslogixOfficeShare}.lock";"${fslogixOfficeShare}.meta";"${fslogixOfficeShare}.metadata"' : '' +var fslogixExclusionsProfileContainers = ';"${fslogixProfileShare}";"${fslogixProfileShare}.lock";"${fslogixProfileShare}.meta";"${fslogixProfileShare}.metadata"' +var fslogixOfficeShare = '\\\\${storageAccountPrefix}??.file.${storageSuffix}\\office-containers\\*\\*.VHDX' +var fslogixProfileShare = '\\\\${storageAccountPrefix}??.file.${storageSuffix}\\profile-containers\\*\\*.VHDX' +var imageReference = empty(imageDefinitionResourceId) ? { + publisher: imagePublisher + offer: imageOffer + sku: imageSku + version: 'latest' +} : { + id: imageDefinitionResourceId +} +var intune = contains(activeDirectorySolution, 'intuneEnrollment') +var nvidiaVmSize = contains(nvidiaVmSizes, virtualMachineSize) +var nvidiaVmSizes = [ + 'Standard_NV6' + 'Standard_NV12' + 'Standard_NV24' + 'Standard_NV12s_v3' + 'Standard_NV24s_v3' + 'Standard_NV48s_v3' + 'Standard_NC4as_T4_v3' + 'Standard_NC8as_T4_v3' + 'Standard_NC16as_T4_v3' + 'Standard_NC64as_T4_v3' + 'Standard_NV6ads_A10_v5' + 'Standard_NV12ads_A10_v5' + 'Standard_NV18ads_A10_v5' + 'Standard_NV36ads_A10_v5' + 'Standard_NV36adms_A10_v5' + 'Standard_NV72ads_A10_v5' +] +var pooledHostPool = (split(hostPoolType, ' ')[0] == 'Pooled') +var securitylogAnalyticsWorkspaceName = securityMonitoring ? split(securityLogAnalyticsWorkspaceResourceId, '/')[8] : '' +var securityLogAnalyticsWorkspaceResourceGroupName = securityMonitoring ? split(securityLogAnalyticsWorkspaceResourceId, '/')[4] : resourceGroup().name +var securityLogAnalyticsWorkspaceSubscriptionId = securityMonitoring ? split(securityLogAnalyticsWorkspaceResourceId, '/')[2] : subscription().subscriptionId +var securityMonitoring = empty(securityLogAnalyticsWorkspaceResourceId) ? false : true +var securityWorkspaceKey = securityMonitoring ? listKeys(securityLogAnalyticsWorkspaceResourceId, '2021-06-01').primarySharedKey : 'NotApplicable' + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-06-01' existing = if (securityMonitoring) { + name: securitylogAnalyticsWorkspaceName + scope: resourceGroup(securityLogAnalyticsWorkspaceSubscriptionId, securityLogAnalyticsWorkspaceResourceGroupName) +} + +resource networkInterface 'Microsoft.Network/networkInterfaces@2020-05-01' = [for i in range(0, sessionHostCount): { + name: '${networkInterfaceNamePrefix}${padLeft((i + sessionHostIndex), 4, '0')}' + location: location + tags: tagsNetworkInterfaces + properties: { + ipConfigurations: [ + { + name: 'ipconfig' + properties: { + privateIPAllocationMethod: 'Dynamic' + subnet: { + id: resourceId(subscription().subscriptionId, virtualNetworkResourceGroup, 'Microsoft.Network/virtualNetworks/subnets', virtualNetwork, subnet) + } + primary: true + privateIPAddressVersion: 'IPv4' + } + } + ] + enableAcceleratedNetworking: acceleratedNetworking == 'True' ? true : false + enableIPForwarding: false + } +}] + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2021-03-01' = [for i in range(0, sessionHostCount): { + name: '${virtualMachineNamePrefix}${padLeft((i + sessionHostIndex), 4, '0')}' + location: location + tags: tagsVirtualMachines + zones: availability == 'AvailabilityZones' ? [ + availabilityZones[i % length(availabilityZones)] + ] : null + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${artifactsUserAssignedIdentityResourceId}': {} + } + } + properties: { + availabilitySet: availability == 'AvailabilitySets' ? { + id: resourceId('Microsoft.Compute/availabilitySets', '${availabilitySetNamePrefix}${padLeft((i + sessionHostIndex) / 200, 2, '0')}') + } : null + hardwareProfile: { + vmSize: virtualMachineSize + } + storageProfile: { + imageReference: imageReference + osDisk: { + name: '${diskNamePrefix}${padLeft((i + sessionHostIndex), 4, '0')}' + osType: 'Windows' + createOption: 'FromImage' + caching: 'ReadWrite' + deleteOption: 'Delete' + managedDisk: { + diskEncryptionSet: { + id: diskEncryptionSetResourceId + } + storageAccountType: diskSku + } + } + dataDisks: [] + } + osProfile: { + computerName: '${virtualMachineNamePrefix}${padLeft((i + sessionHostIndex), 4, '0')}' + adminUsername: virtualMachineUsername + adminPassword: virtualMachinePassword + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: false + } + secrets: [] + allowExtensionOperations: true + } + networkProfile: { + networkInterfaces: [ + { + id: resourceId('Microsoft.Network/networkInterfaces', '${networkInterfaceNamePrefix}${padLeft((i + sessionHostIndex), 4, '0')}') + properties: { + deleteOption: 'Delete' + } + } + ] + } + securityProfile: { + uefiSettings: { + secureBootEnabled: true + vTpmEnabled: true + } + securityType: 'trustedLaunch' + encryptionAtHost: true + } + diagnosticsProfile: { + bootDiagnostics: { + enabled: false + } + } + licenseType: ((imagePublisher == 'MicrosoftWindowsDesktop') ? 'Windows_Client' : 'Windows_Server') + } + dependsOn: [ + networkInterface + ] +}] + +resource extension_IaasAntimalware 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = [for i in range(0, sessionHostCount): { + parent: virtualMachine[i] + name: 'IaaSAntimalware' + location: location + tags: tagsVirtualMachines + properties: { + publisher: 'Microsoft.Azure.Security' + type: 'IaaSAntimalware' + typeHandlerVersion: '1.3' + autoUpgradeMinorVersion: true + enableAutomaticUpgrade: false + settings: { + AntimalwareEnabled: true + RealtimeProtectionEnabled: 'true' + ScheduledScanSettings: { + isEnabled: 'true' + day: '7' // Day of the week for scheduled scan (1-Sunday, 2-Monday, ..., 7-Saturday) + time: '120' // When to perform the scheduled scan, measured in minutes from midnight (0-1440). For example: 0 = 12AM, 60 = 1AM, 120 = 2AM. + scanType: 'Quick' //Indicates whether scheduled scan setting type is set to Quick or Full (default is Quick) + } + Exclusions: fslogix ? { + Paths: fslogixExclusions + } : {} + } + } +}] + +resource extension_GuestAttestation 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = [for i in range(0, sessionHostCount): { + parent: virtualMachine[i] + name: 'GuestAttestation' + location: location + properties: { + publisher: 'Microsoft.Azure.Security.WindowsAttestation' + type: 'GuestAttestation' + typeHandlerVersion: '1.0' + autoUpgradeMinorVersion: true + settings: { + AttestationConfig: { + MaaSettings: { + maaEndpoint: '' + maaTenantName: 'GuestAttestation' + } + AscSettings: { + ascReportingEndpoint: '' + ascReportingFrequency: '' + } + useCustomToken: 'false' + disableAlerts: 'false' + } + } + } +}] + +resource extension_MicrosoftMonitoringAgent 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = [for i in range(0, sessionHostCount): if (monitoring && virtualMachineMonitoringAgent == 'LogAnalyticsAgent') { + parent: virtualMachine[i] + name: 'MicrosoftmonitoringAgent' + location: location + tags: tagsVirtualMachines + properties: { + publisher: 'Microsoft.EnterpriseCloud.monitoring' + type: 'MicrosoftmonitoringAgent' + typeHandlerVersion: '1.0' + autoUpgradeMinorVersion: true + settings: { + workspaceId: monitoring ? reference(resourceId(resourceGroupManagement, 'Microsoft.OperationalInsights/workspaces', logAnalyticsWorkspaceName), '2015-03-20').customerId : null + } + protectedSettings: { + workspaceKey: monitoring ? listKeys(resourceId(resourceGroupManagement, 'Microsoft.OperationalInsights/workspaces', logAnalyticsWorkspaceName), '2015-03-20').primarySharedKey : null + } + } + dependsOn: [ + extension_IaasAntimalware + ] +}] + +resource extension_AzureMonitorWindowsAgent 'Microsoft.Compute/virtualMachines/extensions@2023-03-01' = [for i in range(0, sessionHostCount): if (monitoring && virtualMachineMonitoringAgent == 'AzureMonitorAgent') { + parent: virtualMachine[i] + name: 'AzureMonitorWindowsAgent' + location: location + tags: tagsVirtualMachines + properties: { + publisher: 'Microsoft.Azure.Monitor' + type: 'AzureMonitorWindowsAgent' + typeHandlerVersion: '1.0' + autoUpgradeMinorVersion: true + enableAutomaticUpgrade: true + } +}] + +resource dataCollectionRuleAssociation 'Microsoft.Insights/dataCollectionRuleAssociations@2022-06-01' = [for i in range(0, sessionHostCount): if (monitoring && virtualMachineMonitoringAgent == 'AzureMonitorAgent') { + scope: virtualMachine[i] + name: dataCollectionRuleAssociationName + properties: { + dataCollectionRuleId: dataCollectionRuleResourceId + description: 'AVD Insights data collection rule association' + } + dependsOn: [ + extension_AzureMonitorWindowsAgent + ] +}] + +resource extension_CustomScriptExtension 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = [for i in range(0, sessionHostCount): { + parent: virtualMachine[i] + name: 'CustomScriptExtension' + location: location + tags: tagsVirtualMachines + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + autoUpgradeMinorVersion: true + settings: { + fileUris: [ + '${artifactsUri}${avdAgentBootLoaderMsiName}' + '${artifactsUri}${avdAgentMsiName}' + '${artifactsUri}Set-SessionHostConfiguration.ps1' + ] + timestamp: timestamp + } + protectedSettings: { + commandToExecute: 'powershell -ExecutionPolicy Unrestricted -File Set-SessionHostConfiguration.ps1 -activeDirectorySolution ${activeDirectorySolution} -amdVmSize ${amdVmSize} -avdAgentBootLoaderMsiName "${avdAgentBootLoaderMsiName}" -avdAgentMsiName "${avdAgentMsiName}" -Environment ${environment().name} -fslogix ${fslogix} -fslogixContainerType ${fslogixContainerType} -hostPoolName ${hostPoolName} -HostPoolRegistrationToken "${reference(resourceId(resourceGroupControlPlane, 'Microsoft.DesktopVirtualization/hostpools', hostPoolName), '2019-12-10-preview').registrationInfo.token}" -imageOffer ${imageOffer} -imagePublisher ${imagePublisher} -netAppFileShares ${netAppFileShares} -nvidiaVmSize ${nvidiaVmSize} -pooledHostPool ${pooledHostPool} -securityMonitoring ${securityMonitoring} -SecurityWorkspaceId ${securityMonitoring ? logAnalyticsWorkspace.properties.customerId : 'NotApplicable'} -securityWorkspaceKey "${securityWorkspaceKey}" -storageAccountPrefix ${storageAccountPrefix} -storageCount ${storageCount} -storageIndex ${storageIndex} -storageService ${storageService} -storageSuffix ${storageSuffix}' + managedidentity: { + clientId: artifactsUserAssignedIdentityClientId + } + } + } + dependsOn: [ + dataCollectionRuleAssociation + extension_MicrosoftMonitoringAgent + ] +}] + +// Enables drain mode on the session hosts so users cannot login to hosts immediately after the deployment +module drainMode '../common/customScriptExtensions.bicep' = if (enableDrainMode) { + name: 'CSE_DrainMode_${batchCount}_${timestamp}' + scope: resourceGroup(resourceGroupManagement) + params: { + fileUris: [ + '${artifactsUri}Set-AvdDrainMode.ps1' + ] + location: location + parameters: '-Environment ${environment().name} -hostPoolName ${hostPoolName} -HostPoolResourceGroupName ${resourceGroupControlPlane} -sessionHostCount ${sessionHostCount} -sessionHostIndex ${sessionHostIndex} -SubscriptionId ${subscription().subscriptionId} -TenantId ${tenant().tenantId} -userAssignedidentityClientId ${deploymentUserAssignedidentityClientId} -virtualMachineNamePrefix ${virtualMachineNamePrefix}' + scriptFileName: 'Set-AvdDrainMode.ps1' + tags: tagsVirtualMachines + userAssignedIdentityClientId: deploymentUserAssignedidentityClientId + virtualMachineName: managementVirtualMachineName + } + dependsOn: [ + extension_CustomScriptExtension + ] +} + +resource extension_JsonADDomainExtension 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = [for i in range(0, sessionHostCount): if (contains(activeDirectorySolution, 'DomainServices')) { + parent: virtualMachine[i] + name: 'JsonADDomainExtension' + location: location + tags: tagsVirtualMachines + properties: { + forceUpdateTag: timestamp + publisher: 'Microsoft.Compute' + type: 'JsonADDomainExtension' + typeHandlerVersion: '1.3' + autoUpgradeMinorVersion: true + settings: { + Name: domainName + Options: '3' + OUPath: organizationalUnitPath + Restart: 'true' + User: domainJoinUserPrincipalName + } + protectedSettings: { + Password: domainJoinPassword + } + } + dependsOn: [ + drainMode + ] +}] + +resource extension_AADLoginForWindows 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = [for i in range(0, sessionHostCount): if (!contains(activeDirectorySolution, 'DomainServices')) { + parent: virtualMachine[i] + name: 'AADLoginForWindows' + location: location + tags: tagsVirtualMachines + properties: { + publisher: 'Microsoft.Azure.ActiveDirectory' + type: 'AADLoginForWindows' + typeHandlerVersion: '2.0' + autoUpgradeMinorVersion: true + settings: intune ? { + mdmId: '0000000a-0000-0000-c000-000000000000' + } : null + } + dependsOn: [ + drainMode + ] +}] + +resource extension_AmdGpuDriverWindows 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = [for i in range(0, sessionHostCount): if (amdVmSize) { + parent: virtualMachine[i] + name: 'AmdGpuDriverWindows' + location: location + tags: tagsVirtualMachines + properties: { + publisher: 'Microsoft.HpcCompute' + type: 'AmdGpuDriverWindows' + typeHandlerVersion: '1.0' + autoUpgradeMinorVersion: true + settings: {} + } + dependsOn: [ + extension_AADLoginForWindows + extension_JsonADDomainExtension + ] +}] + +resource extension_NvidiaGpuDriverWindows 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = [for i in range(0, sessionHostCount): if (nvidiaVmSize) { + parent: virtualMachine[i] + name: 'NvidiaGpuDriverWindows' + location: location + tags: tagsVirtualMachines + properties: { + publisher: 'Microsoft.HpcCompute' + type: 'NvidiaGpuDriverWindows' + typeHandlerVersion: '1.2' + autoUpgradeMinorVersion: true + settings: {} + } + dependsOn: [ + extension_AADLoginForWindows + extension_JsonADDomainExtension + ] +}] diff --git a/src/bicep/add-ons/azureVirtualDesktop/parameters.json b/src/bicep/add-ons/azureVirtualDesktop/parameters.json new file mode 100644 index 000000000..e5780903d --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/parameters.json @@ -0,0 +1,225 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "activeDirectorySolution": { + "value": "ActiveDirectoryDomainServices" + }, + "artifactsContainerName": { + "value": "artifacts" + }, + "artifactsStorageAccountResourceId": { + "value": "" + }, + "automationAccountPrivateDnsZoneResourceId": { + "value": "" + }, + "availability": { + "value": "AvailabilityZones" + }, + "avdAgentMsiName": { + "value": "Microsoft.RDInfra.RDAgent.Installer-x64-1.0.7539.8300.msi" + }, + "avdAgentBootLoaderMsiName": { + "value": "Microsoft.RDInfra.RDAgentBootLoader.Installer-x64 (5).msi" + }, + "avdObjectId": { + "value": "cdcfb416-e2fe-41e2-be12-33813c1cd427" + }, + "avdPrivateDnsZoneResourceId": { + "value": "/subscriptions/3764b123-4849-4395-8e6e-ca6d68d8d4b4/resourceGroups/rg-net-d-eu/providers/Microsoft.Network/privateDnsZones/privatelink.wvd.microsoft.com" + }, + "azureFilesPrivateDnsZoneResourceId": { + "value": "/subscriptions/3764b123-4849-4395-8e6e-ca6d68d8d4b4/resourceGroups/rg-net-d-eu/providers/Microsoft.Network/privateDnsZones/privatelink.file.core.windows.net" + }, + "azurePowerShellModuleMsiName": { + "value": "Az-Cmdlets-10.2.0.37547-x64.msi" + }, + "customRdpProperty": { + "value": "audiocapturemode:i:1;camerastoredirect:s:*;use multimon:i:0;drivestoredirect:s:;" + }, + "disableBgpRoutePropagation": { + "value": false + }, + "diskEncryption": { + "value": true + }, + "diskSku": { + "value": "Premium_LRS" + }, + "domainJoinPassword": { + "reference": { + "keyVault": { + "id": "/subscriptions/3764b123-4849-4395-8e6e-ca6d68d8d4b4/resourceGroups/rg-core-d-eu/providers/Microsoft.KeyVault/vaults/kv-core-d-eu" + }, + "secretName": "DomainJoinPassword" + } + }, + "domainJoinUserPrincipalName": { + "reference": { + "keyVault": { + "id": "/subscriptions/3764b123-4849-4395-8e6e-ca6d68d8d4b4/resourceGroups/rg-core-d-eu/providers/Microsoft.KeyVault/vaults/kv-core-d-eu" + }, + "secretName": "DomainJoinUsername" + } + }, + "domainName": { + "value": "jasonmasten.com" + }, + "drainMode": { + "value": true + }, + "environment": { + "value": "d" + }, + "fslogixShareSizeInGB": { + "value": 100 + }, + "fslogixSolution": { + "value": "ProfileContainer" + }, + "fslogixStorage": { + "value": "AzureFiles Premium" + }, + "globalWorkspacePrivateDnsZoneResourceId": { + "value": "/subscriptions/3764b123-4849-4395-8e6e-ca6d68d8d4b4/resourceGroups/rg-net-d-eu/providers/Microsoft.Network/privateDnsZones/privatelink-global.wvd.microsoft.com" + }, + "hostPoolPublicNetworkAccess": { + "value": "Enabled" + }, + "hostPoolType": { + "value": "Pooled DepthFirst" + }, + "hubAzureFirewallResourceId": { + "value": "" + }, + "hubVirtualNetworkResourceId": { + "value": "" + }, + "identifier": { + "value": "jnm" + }, + "imageDefinitionResourceId": { + "value": "" + }, + "imageOffer": { + "value": "office-365" + }, + "imagePublisher": { + "value": "MicrosoftWindowsDesktop" + }, + "imageSku": { + "value": "win11-22h2-avd-m365" + }, + "locationControlPlane": { + "value": "eastus" + }, + "logAnalyticsWorkspaceRetention": { + "value": 30 + }, + "logAnalyticsWorkspaceSku": { + "value": "PerGB2018" + }, + "maxSessionLimit": { + "value": 8 + }, + "monitoring": { + "value": true + }, + "organizationalUnitPath": { + "value": "OU=AVD,DC=jasonmasten,DC=com" + }, + "recoveryServices": { + "value": false + }, + "scalingBeginPeakTime": { + "value": "7:00" + }, + "scalingEndPeakTime": { + "value": "18:00" + }, + "scalingLimitSecondsToForceLogOffUser": { + "value": "0" + }, + "scalingMinimumNumberOfRdsh": { + "value": "0" + }, + "scalingSessionThresholdPerCPU": { + "value": "1" + }, + "scalingTool": { + "value": true + }, + "securityLogAnalyticsWorkspaceResourceId": { + "value": "" + }, + "securityPrincipalObjectIds": { + "value": [ + "06c4afa5-b6dc-4719-8034-947887108e29" + ] + }, + "securityPrincipals": { + "value": [] + }, + "sessionDesktopFriendlyName": { + "value": "" + }, + "sessionHostCount": { + "value": 1 + }, + "sessionHostIndex": { + "value": 0 + }, + "stampIndex": { + "value": 0 + }, + "storageCount": { + "value": 1 + }, + "storageIndex": { + "value": 0 + }, + "subnetAddressPrefix": { + "value": "10.0.8.0/24" + }, + "subnetResourceId": { + "value": "/subscriptions/3764b123-4849-4395-8e6e-ca6d68d8d4b4/resourceGroups/rg-net-d-eu/providers/Microsoft.Network/virtualNetworks/vnet-net-d-eu/subnets/Clients" + }, + "tags": { + "value": {} + }, + "validationEnvironment": { + "value": false + }, + "virtualMachinePassword": { + "reference": { + "keyVault": { + "id": "/subscriptions/3764b123-4849-4395-8e6e-ca6d68d8d4b4/resourceGroups/rg-core-d-eu/providers/Microsoft.KeyVault/vaults/kv-core-d-eu" + }, + "secretName": "VmPassword" + } + }, + "virtualMachineSize": { + "value": "Standard_D4ads_v5" + }, + "virtualMachineUsername": { + "reference": { + "keyVault": { + "id": "/subscriptions/3764b123-4849-4395-8e6e-ca6d68d8d4b4/resourceGroups/rg-core-d-eu/providers/Microsoft.KeyVault/vaults/kv-core-d-eu" + }, + "secretName": "VmUsername" + } + }, + "virtualNetworkAddressPrefix": { + "value": [ + "10.0.8.0/24" + ] + }, + "workspaceFriendlyName": { + "value": "Jason Masten" + }, + "workspacePublicNetworkAccess": { + "value": "Enabled" + } + } +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/solution.bicep b/src/bicep/add-ons/azureVirtualDesktop/solution.bicep new file mode 100644 index 000000000..dd837de9c --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/solution.bicep @@ -0,0 +1,659 @@ +targetScope = 'subscription' + +@allowed([ + 'ActiveDirectoryDomainServices' + 'MicrosoftEntraDomainServices' + 'MicrosoftEntraId' + 'MicrosoftEntraIdIntuneEnrollment' +]) +@description('The service providing domain services for Azure Virtual Desktop. This is needed to properly configure the session hosts and if applicable, the Azure Storage Account.') +param activeDirectorySolution string + +@description('The name of the Azure Blobs container hosting the required artifacts.') +param artifactsContainerName string + +@description('The resource ID for the storage account hosting the artifacts in Blob storage.') +param artifactsStorageAccountResourceId string + +@allowed([ + 'AvailabilitySets' + 'AvailabilityZones' + 'None' +]) +@description('The desired availability option when deploying a pooled host pool. The best practice is to deploy to availability zones for the highest resilency and service level agreement.') +param availability string = 'AvailabilityZones' + +@description('The blob name of the MSI file for the AVD Agent installer. The file must be hosted in an Azure Blobs container with the other deployment artifacts.') +param avdAgentMsiName string + +@description('The blob name of the MSI file for the AVD Agent Boot Loader installer. The file must be hosted in an Azure Blobs container with the other deployment artifacts.') +param avdAgentBootLoaderMsiName string + +@description('The object ID for the Azure Virtual Desktop enterprise application in Microsoft Entra ID. The object ID can found by selecting Microsoft Applications using the Application type filter in the Enterprise Applications blade of Microsoft Entra ID.') +param avdObjectId string + +@description('The subnet address prefix for the Azure NetApp Files delegated subnet.') +param azureNetAppFilesSubnetAddressPrefix string = '' + +@description('The blob name of the MSI file for the Azure PowerShell Module installer. The file must be hosted in an Azure Blobs container with the other deployment artifacts.') +param azurePowerShellModuleMsiName string + +@description('The RDP properties to add or remove RDP functionality on the AVD host pool. The string must end with a semi-colon. Settings reference: https://learn.microsoft.com/windows-server/remote/remote-desktop-services/clients/rdp-files') +param customRdpProperty string = 'audiocapturemode:i:1;camerastoredirect:s:*;use multimon:i:0;drivestoredirect:s:;' + +@description('The friendly name for the Desktop application in the desktop application group.') +param desktopFriendlyName string = '' + +@description('Disabling BGP route propagation is a route table configuration that prevents the propagation of on-premises routes to network interfaces in the associated subnets.') +param disableBgpRoutePropagation bool = true + +@allowed([ + 'Standard_LRS' + 'StandardSSD_LRS' + 'Premium_LRS' +]) +@description('The storage SKU for the managed disks on the AVD session hosts. Production deployments should use Premium_LRS.') +param diskSku string = 'Premium_LRS' + +@secure() +@description('The password for the account to domain join the AVD session hosts.') +param domainJoinPassword string = '' + +@description('The user principal name for the account to domain join the AVD session hosts.') +param domainJoinUserPrincipalName string = '' + +@description('The name of the domain that provides ADDS to the AVD session hosts.') +param domainName string = '' + +@description('The drain mode option enables drain mode for the sessions hosts in this deployment to prevent users from accessing the hosts until they have been validated.') +param drainMode bool = false + +@allowed([ + 'dev' // Development + 'prod' // Production + 'test' // Test +]) +@description('The short name for the target environment.') +param environmentShortName string = 'dev' + +@description('The file share size(s) in GB for the Fslogix storage solution.') +param fslogixShareSizeInGB int = 100 + +@allowed([ + 'CloudCacheProfileContainer' // FSLogix Cloud Cache Profile Container + 'CloudCacheProfileOfficeContainer' // FSLogix Cloud Cache Profile & Office Container + 'ProfileContainer' // FSLogix Profile Container + 'ProfileOfficeContainer' // FSLogix Profile & Office Container +]) +@description('If deploying FSLogix, select the desired type of container for user profiles. https://learn.microsoft.com/en-us/fslogix/concepts-container-types') +param fslogixContainerType string = 'ProfileContainer' + +@allowed([ + 'AzureNetAppFiles Premium' // ANF with the Premium SKU, 450,000 IOPS + 'AzureNetAppFiles Standard' // ANF with the Standard SKU, 320,000 IOPS + 'AzureFiles Premium' // Azure Files Premium with a Private Endpoint, 100,000 IOPS + 'AzureFiles Standard' // Azure Files Standard with the Large File Share option and a Private Endpoint, 20,000 IOPS + 'None' +]) +@description('Enable an Fslogix storage option to manage user profiles for the AVD session hosts. The selected service & SKU should provide sufficient IOPS for all of your users. https://docs.microsoft.com/en-us/azure/architecture/example-scenario/wvd/windows-virtual-desktop-fslogix#performance-requirements') +param fslogixStorageService string = 'AzureFiles Standard' + +@allowed([ + 'Disabled' + 'Enabled' + 'EnabledForClientsOnly' + 'EnabledForSessionHostsOnly' +]) +@description('The type of public network access for the host pool.') +param hostPoolPublicNetworkAccess string + +@allowed([ + 'Pooled DepthFirst' + 'Pooled BreadthFirst' + 'Personal Automatic' + 'Personal Direct' +]) +@description('These options specify the host pool type and depending on the type provides the load balancing options and assignment types.') +param hostPoolType string = 'Pooled DepthFirst' + +@description('The resource ID for the Azure Firewall in the HUB subscription') +param hubAzureFirewallResourceId string + +@description('The resource ID for the subnet in the Shared Services subscription. This is required for the private endpoint on the AVD Global Workspace.') +param hubSubnetResourceId string + +@description('The resource ID for the Azure Virtual Network in the HUB subscription.') +param hubVirtualNetworkResourceId string + +@maxLength(3) +@description('The unique identifier between each business unit or project supporting AVD in your tenant. This is the unique naming component between each AVD stamp.') +param identifier string = 'avd' + +@description('The resource ID for the Compute Gallery Image Version. Do not set this value if using a marketplace image.') +param imageDefinitionResourceId string = '' + +@description('Offer for the virtual machine image') +param imageOffer string = 'office-365' + +@description('Publisher for the virtual machine image') +param imagePublisher string = 'MicrosoftWindowsDesktop' + +@description('SKU for the virtual machine image') +param imageSku string = 'win11-22h2-avd-m365' + +@description('The deployment location for the AVD management resources.') +param locationControlPlane string = deployment().location + +@description('The deployment location for the AVD sessions hosts.') +param locationVirtualMachines string = deployment().location + +@maxValue(730) +@minValue(30) +@description('The retention for the Log Analytics Workspace to setup the AVD monitoring solution') +param logAnalyticsWorkspaceRetention int = 30 + +@allowed([ + 'Free' + 'Standard' + 'Premium' + 'PerNode' + 'PerGB2018' + 'Standalone' + 'CapacityReservation' +]) +@description('The SKU for the Log Analytics Workspace to setup the AVD monitoring solution') +param logAnalyticsWorkspaceSku string = 'PerGB2018' + +@description('The maximum number of sessions per AVD session host.') +param maxSessionLimit int + +@description('Deploys the required monitoring resources to enable AVD Insights and monitor features in the automation account.') +param monitoring bool = true + +@description('The distinguished name for the target Organization Unit in Active Directory Domain Services.') +param organizationalUnitPath string = '' + +@description('Enable backups to an Azure Recovery Services vault. For a pooled host pool this will enable backups on the Azure file share. For a personal host pool this will enable backups on the AVD sessions hosts.') +param recoveryServices bool = false + +@description('The time when session hosts will scale up and continue to stay on to support peak demand; Format 24 hours e.g. 9:00 for 9am') +param scalingBeginPeakTime string = '9:00' + +@description('The time when session hosts will scale down and stay off to support low demand; Format 24 hours e.g. 17:00 for 5pm') +param scalingEndPeakTime string = '17:00' + +@description('The number of seconds to wait before automatically signing out users. If set to 0 any session host that has user sessions will be left untouched') +param scalingLimitSecondsToForceLogOffUser string = '0' + +@description('The minimum number of session host VMs to keep running during off-peak hours. The scaling tool will not work if all virtual machines are turned off and the Start VM On Connect solution is not enabled.') +param scalingMinimumNumberOfRdsh string = '0' + +@description('The maximum number of sessions per CPU that will be used as a threshold to determine when new session host VMs need to be started during peak hours') +param scalingSessionThresholdPerCPU string = '1' + +@description('Deploys the required resources for the Scaling Tool. https://docs.microsoft.com/en-us/azure/virtual-desktop/scaling-automation-logic-apps') +param scalingTool bool = false + +@description('The resource ID of the log analytics workspace used for Azure Sentinel and / or Defender for Cloud. When using the Microsoft monitoring Agent, this allows you to multihome the agent to reduce unnecessary log collection and reduce cost.') +param securityLogAnalyticsWorkspaceResourceId string = '' + +@description('The array of Security Principals with their object IDs and display names to assign to the AVD Application Group and FSLogix Storage.') +param securityPrincipals array + +@maxValue(5000) +@minValue(0) +@description('The number of session hosts to deploy in the host pool. Ensure you have the approved quota to deploy the desired count.') +param sessionHostCount int = 1 + +@maxValue(4999) +@minValue(0) +@description('The starting number for the session hosts. This is important when adding virtual machines to ensure an update deployment is not performed on an existing, active session host.') +param sessionHostIndex int = 0 + +@maxValue(9) +@minValue(0) +@description('The stamp index allows for multiple AVD stamps with the same business unit or project to support different use cases. For example, "0" could be used for an office workers host pool and "1" could be used for a developers host pool within the "finance" business unit.') +param stampIndex int = 0 + +@maxValue(100) +@minValue(0) +@description('The number of storage accounts to deploy to support sharding across multiple storage accounts. https://docs.microsoft.com/en-us/azure/architecture/patterns/sharding') +param storageCount int = 1 + +@maxValue(99) +@minValue(0) +@description('The starting number for the names of the storage accounts to support sharding across multiple storage accounts. https://docs.microsoft.com/en-us/azure/architecture/patterns/sharding') +param storageIndex int = 0 + +@description('The address prefix(es) for the new subnet(s) that will be created in the spoke virtual network(s).') +param subnetAddressPrefixes array + +@description('The Key / value pairs of metadata for the Azure resource groups and resources.') +param tags object = {} + +@description('DO NOT MODIFY THIS VALUE! The timestamp is needed to differentiate deployments for certain Azure resources and must be set using a parameter.') +param timestamp string = utcNow('yyyyMMddhhmmss') + +@description('The validation environment setting on the AVD host pool determines whether the hostpool should receive AVD preview features for testing.') +param validationEnvironment bool = false + +@allowed([ + 'AzureMonitorAgent' + 'LogAnalyticsAgent' +]) +@description('Input the desired monitoring agent to send events and performance counters to a log analytics workspace.') +param virtualMachineMonitoringAgent string = 'LogAnalyticsAgent' + +@secure() +@description('The local administrator password for the AVD session hosts') +param virtualMachinePassword string + +@description('The virtual machine SKU for the AVD session hosts.') +param virtualMachineSize string = 'Standard_D4ads_v5' + +@description('The local administrator username for the AVD session hosts') +param virtualMachineUsername string + +@description('The address prefix for the new spoke virtual network(s).') +param virtualNetworkAddressPrefixes array + +@description('The friendly name for the AVD workspace that is displayed in the end-user client.') +param workspaceFriendlyName string = '' + +@allowed([ + 'Disabled' + 'Enabled' +]) +@description('The public network access setting on the AVD workspace either disables public network access or allows both public and private network access.') +param workspacePublicNetworkAccess string + +var artifactsUri = 'https://${artifactsStorageAccountName}.blob.${environment().suffixes.storage}/${artifactsContainerName}/' +var artifactsStorageAccountName = split(artifactsStorageAccountResourceId, '/')[8] +var privateDnsZoneResourceIdPrefix = '/subscriptions/${split(hubVirtualNetworkResourceId, '/')[2]}/resourceGroups/${split(hubVirtualNetworkResourceId, '/')[4]}/providers/Microsoft.Network/privateDnsZones/' +var deploymentLocations = union([ + locationControlPlane +], [ + locationVirtualMachines +]) +var resourceGroupsCount = 4 + length(deploymentLocations) + (fslogixStorageService == 'None' ? 0 : 1) + +// Resource Names +module resourceNames 'modules/resourceNames.bicep' = { + name: 'ResourceNames_${timestamp}' + params: { + environmentShortName: environmentShortName + identifier: identifier + locationControlPlane: locationControlPlane + locationVirtualMachines: locationVirtualMachines + stampIndex: stampIndex + } +} + +// Logic +module logic 'modules/logic.bicep' = { + name: 'Logic_${timestamp}' + params: { + activeDirectorySolution: activeDirectorySolution + deploymentLocations: deploymentLocations + diskSku: diskSku + domainName: domainName + fileShareNames: resourceNames.outputs.fileShareNames + fslogixContainerType: fslogixContainerType + fslogixStorageService: fslogixStorageService + hostPoolType: hostPoolType + imageOffer: imageOffer + imagePublisher: imagePublisher + imageSku: imageSku + locations: resourceNames.outputs.locations + locationVirtualMachines: locationVirtualMachines + resourceGroupControlPlane: resourceNames.outputs.resourceGroupControlPlane + resourceGroupFeedWorkspace: resourceNames.outputs.resourceGroupFeedWorkspace + resourceGroupHosts: resourceNames.outputs.resourceGroupHosts + resourceGroupManagement: resourceNames.outputs.resourceGroupManagement + resourceGroupsNetwork: resourceNames.outputs.resourceGroupsNetwork + resourceGroupStorage: resourceNames.outputs.resourceGroupStorage + securityPrincipals: securityPrincipals + sessionHostCount: sessionHostCount + sessionHostIndex: sessionHostIndex + virtualMachineNamePrefix: resourceNames.outputs.virtualMachineNamePrefix + virtualMachineSize: virtualMachineSize + } +} + +// Resource Groups +module rgs 'modules/resourceGroup.bicep' = [for i in range(0, resourceGroupsCount): { + name: 'ResourceGroup_${i}_${timestamp}' + params: { + location: contains(logic.outputs.resourceGroups[i], 'controlPlane') || contains(logic.outputs.resourceGroups[i], 'feedWorkspace') ? locationControlPlane : locationVirtualMachines + resourceGroupName: logic.outputs.resourceGroups[i] + tags: tags + } +}] + +module network_controlPlane 'modules/network/networking.bicep' = { + name: 'Network_ControlPlane_${timestamp}' + params: { + azureNetAppFilesSubnetAddressPrefix: !empty(azureNetAppFilesSubnetAddressPrefix) && length(deploymentLocations) == 1 ? azureNetAppFilesSubnetAddressPrefix : '' + disableBgpRoutePropagation: disableBgpRoutePropagation + hubAzureFirewallResourceId: hubAzureFirewallResourceId + hubVirtualNetworkResourceId: hubVirtualNetworkResourceId + index: 0 + location: deploymentLocations[0] + networkSecurityGroupName: resourceNames.outputs.networkSecurityGroupNames[0] + resourceGroupNetwork: resourceNames.outputs.resourceGroupsNetwork[0] + routeTableName: resourceNames.outputs.routeTables[0] + subnetAddressPrefixes: subnetAddressPrefixes + timestamp: timestamp + virtualNetworkAddressPrefixes: virtualNetworkAddressPrefixes + virtualNetworkName: resourceNames.outputs.virtulNetworkNames[0] + } + dependsOn: [ + rgs + ] +} + +module network_hosts 'modules/network/networking.bicep' = if (length(deploymentLocations) == 2) { + name: 'Network_Hosts_${timestamp}' + params: { + azureNetAppFilesSubnetAddressPrefix: !empty(azureNetAppFilesSubnetAddressPrefix) && length(deploymentLocations) == 2 ? azureNetAppFilesSubnetAddressPrefix : '' + disableBgpRoutePropagation: disableBgpRoutePropagation + hubAzureFirewallResourceId: hubAzureFirewallResourceId + hubVirtualNetworkResourceId: hubVirtualNetworkResourceId + index: 1 + location: deploymentLocations[1] + networkSecurityGroupName: resourceNames.outputs.networkSecurityGroupNames[1] + resourceGroupNetwork: length(deploymentLocations) == 1 ? resourceNames.outputs.resourceGroupsNetwork[0] : resourceNames.outputs.resourceGroupsNetwork[1] + routeTableName: resourceNames.outputs.routeTables[1] + subnetAddressPrefixes: subnetAddressPrefixes + timestamp: timestamp + virtualNetworkAddressPrefixes: virtualNetworkAddressPrefixes + virtualNetworkName: resourceNames.outputs.virtulNetworkNames[1] + } + dependsOn: [ + rgs + ] +} + +// Management Services: Logging, Automation, Keys, Encryption +module management 'modules/management/management.bicep' = { + name: 'Management_${timestamp}' + params: { + activeDirectorySolution: activeDirectorySolution + artifactsStorageAccountResourceId: artifactsStorageAccountResourceId + artifactsUri: artifactsUri + automationAccountName: resourceNames.outputs.automationAccountName + automationAccountPrivateDnsZoneResourceId: '${privateDnsZoneResourceIdPrefix}${resourceNames.outputs.azureAutomationPrivateDnsZoneName}' + availability: availability + avdObjectId: avdObjectId + azureBlobsPrivateDnsZoneResourceId: '${privateDnsZoneResourceIdPrefix}${resourceNames.outputs.blobPrivateDnsZoneName}' + azurePowerShellModuleMsiName: azurePowerShellModuleMsiName + azureQueueStoragePrivateDnsZoneResourceId: '${privateDnsZoneResourceIdPrefix}${resourceNames.outputs.queuePrivateDnsZoneName}' + dataCollectionRuleName: resourceNames.outputs.dataCollectionRuleName + //diskAccessName: resourceNames.outputs.diskAccessName + diskEncryptionSetName: resourceNames.outputs.diskEncryptionSetName + diskNamePrefix: resourceNames.outputs.diskNamePrefix + diskSku: diskSku + domainJoinPassword: domainJoinPassword + domainJoinUserPrincipalName: domainJoinUserPrincipalName + domainName: domainName + enableMonitoring: monitoring + environmentShortName: environmentShortName + fslogix: logic.outputs.fslogix + fslogixStorageService: fslogixStorageService + hostPoolName: resourceNames.outputs.hostPoolName + hostPoolType: hostPoolType + imageDefinitionResourceId: imageDefinitionResourceId + keyVaultAbbreviation: resourceNames.outputs.resourceAbbreviations.keyVaults + keyVaultName: resourceNames.outputs.keyVaultName + keyVaultPrivateDnsZoneResourceId: '${privateDnsZoneResourceIdPrefix}${resourceNames.outputs.keyVaultPrivateDnsZoneName}' + locationVirtualMachines: locationVirtualMachines + logAnalyticsWorkspaceName: resourceNames.outputs.logAnalyticsWorkspaceName + logAnalyticsWorkspaceRetention: logAnalyticsWorkspaceRetention + logAnalyticsWorkspaceSku: logAnalyticsWorkspaceSku + networkInterfaceNamePrefix: resourceNames.outputs.networkInterfaceNamePrefix + organizationalUnitPath: organizationalUnitPath + recoveryServices: recoveryServices + recoveryServicesPrivateDnsZoneResourceId: '${privateDnsZoneResourceIdPrefix}${resourceNames.outputs.backupPrivateDnsZoneName}' + recoveryServicesVaultName: resourceNames.outputs.recoveryServicesVaultName + resourceGroupControlPlane: resourceNames.outputs.resourceGroupControlPlane + resourceGroupFeedWorkspace: resourceNames.outputs.resourceGroupFeedWorkspace + resourceGroupHosts: resourceNames.outputs.resourceGroupHosts + resourceGroupManagement: resourceNames.outputs.resourceGroupManagement + resourceGroupStorage: resourceNames.outputs.resourceGroupStorage + roleDefinitions: logic.outputs.roleDefinitions + scalingTool: scalingTool + securityLogAnalyticsWorkspaceResourceId: securityLogAnalyticsWorkspaceResourceId + sessionHostCount: sessionHostCount + storageService: logic.outputs.storageService + subnetResourceId: length(deploymentLocations) == 1 ? network_controlPlane.outputs.subnetResourceId : network_hosts.outputs.subnetResourceId + tags: tags + timestamp: timestamp + timeZone: logic.outputs.timeZone + userAssignedIdentityNamePrefix: resourceNames.outputs.userAssignedIdentityNamePrefix + virtualMachineMonitoringAgent: virtualMachineMonitoringAgent + virtualMachineNamePrefix: resourceNames.outputs.virtualMachineNamePrefix + virtualMachinePassword: virtualMachinePassword + virtualMachineSize: virtualMachineSize + virtualMachineUsername: virtualMachineUsername + workspaceNamePrefix: resourceNames.outputs.workspaceFeedNamePrefix + } + dependsOn: [ + rgs + ] +} + +// Global AVD Worksspace +// This module creates the global AVD workspace to support AVD with Private Link +module hub 'modules/hub/hub.bicep' = { + name: 'Hub_${timestamp}' + scope: subscription(split(hubSubnetResourceId, '/')[2]) + params: { + existingWorkspace: management.outputs.existingFeedWorkspace + globalWorkspacePrivateDnsZoneResourceId: '${privateDnsZoneResourceIdPrefix}${resourceNames.outputs.avdGlobalPrivateDnsZoneName}' + hubSubnetResourceId: hubSubnetResourceId + resourceGroupName: resourceNames.outputs.resourceGroupGlobalWorkspace + timestamp: timestamp + workspaceNamePrefix: resourceNames.outputs.workspaceGlobalNamePrefix + } +} + +// AVD Control Plane +// This module deploys the host pool and desktop application group +module controlPlane 'modules/controlPlane/controlPlane.bicep' = { + name: 'ControlPlane_${timestamp}' + params: { + activeDirectorySolution: activeDirectorySolution + artifactsUri: artifactsUri + avdPrivateDnsZoneResourceId: '${privateDnsZoneResourceIdPrefix}${resourceNames.outputs.avdPrivateDnsZoneName}' + customRdpProperty: customRdpProperty + deploymentUserAssignedIdentityClientId: management.outputs.deploymentUserAssignedIdentityClientId + desktopApplicationGroupName: resourceNames.outputs.desktopApplicationGroupName + desktopFriendlyName: desktopFriendlyName + existingFeedWorkspace: management.outputs.existingFeedWorkspace + hostPoolName: resourceNames.outputs.hostPoolName + hostPoolPublicNetworkAccess: hostPoolPublicNetworkAccess + hostPoolType: hostPoolType + locationControlPlane: locationControlPlane + locationVirtualMachines: locationVirtualMachines + logAnalyticsWorkspaceResourceId: monitoring ? management.outputs.logAnalyticsWorkspaceResourceId : '' + managementVirtualMachineName: management.outputs.virtualMachineName + maxSessionLimit: maxSessionLimit + monitoring: monitoring + resourceGroupControlPlane: resourceNames.outputs.resourceGroupControlPlane + resourceGroupFeedWorkspace: resourceNames.outputs.resourceGroupFeedWorkspace + resourceGroupManagement: resourceNames.outputs.resourceGroupManagement + roleDefinitions: logic.outputs.roleDefinitions + securityPrincipalObjectIds: map(securityPrincipals, item => item.objectId) + subnetResourceId: network_controlPlane.outputs.subnetResourceId + tags: tags + timestamp: timestamp + validationEnvironment: validationEnvironment + vmTemplate: logic.outputs.vmTemplate + workspaceFriendlyName: workspaceFriendlyName + workspaceNamePrefix: resourceNames.outputs.workspaceFeedNamePrefix + workspacePublicNetworkAccess: workspacePublicNetworkAccess + } + dependsOn: [ + rgs + ] +} + +module fslogix 'modules/fslogix/fslogix.bicep' = { + name: 'FSLogix_${timestamp}' + params: { + activeDirectoryConnection: management.outputs.validateANFfActiveDirectory + activeDirectorySolution: activeDirectorySolution + artifactsUri: artifactsUri + automationAccountName: resourceNames.outputs.automationAccountName + availability: availability + azureFilesPrivateDnsZoneResourceId: '${privateDnsZoneResourceIdPrefix}${resourceNames.outputs.filePrivateDnsZoneName}' + delegatedSubnetId: management.outputs.validateANFSubnetId + deploymentUserAssignedIdentityClientId: management.outputs.deploymentUserAssignedIdentityClientId + dnsServers: management.outputs.validateANFDnsServers + domainJoinPassword: domainJoinPassword + domainJoinUserPrincipalName: domainJoinUserPrincipalName + domainName: domainName + encryptionUserAssignedIdentityResourceId: management.outputs.encryptionUserAssignedIdentityResourceId + fileShares: logic.outputs.fileShares + fslogixContainerType: fslogixContainerType + fslogixShareSizeInGB: fslogixShareSizeInGB + fslogixStorageService: fslogixStorageService + hostPoolName: resourceNames.outputs.hostPoolName + hostPoolType: hostPoolType + keyVaultUri: management.outputs.keyVaultUri + location: locationVirtualMachines + managementVirtualMachineName: management.outputs.virtualMachineName + netAppAccountName: resourceNames.outputs.netAppAccountName + netAppCapacityPoolName: resourceNames.outputs.netAppCapacityPoolName + netbios: logic.outputs.netbios + organizationalUnitPath: organizationalUnitPath + recoveryServices: recoveryServices + recoveryServicesVaultName: resourceNames.outputs.recoveryServicesVaultName + resourceGroupControlPlane: resourceNames.outputs.resourceGroupControlPlane + resourceGroupManagement: resourceNames.outputs.resourceGroupManagement + resourceGroupStorage: resourceNames.outputs.resourceGroupStorage + securityPrincipalNames: map(securityPrincipals, item => item.name) + securityPrincipalObjectIds: map(securityPrincipals, item => item.objectId) + smbServerLocation: logic.outputs.smbServerLocation + storageAccountNamePrefix: resourceNames.outputs.storageAccountNamePrefix + storageCount: storageCount + storageEncryptionKeyName: management.outputs.storageEncryptionKeyName + storageIndex: storageIndex + storageService: logic.outputs.storageService + storageSku: logic.outputs.storageSku + subnet: length(deploymentLocations) == 1 ? split(network_controlPlane.outputs.subnetResourceId, '/')[10] : split(network_hosts.outputs.subnetResourceId, '/')[10] + tags: tags + timestamp: timestamp + timeZone: logic.outputs.timeZone + virtualNetwork: length(deploymentLocations) == 1 ? split(network_controlPlane.outputs.subnetResourceId, '/')[8] : split(network_hosts.outputs.subnetResourceId, '/')[8] + virtualNetworkResourceGroup: length(deploymentLocations) == 1 ? split(network_controlPlane.outputs.subnetResourceId, '/')[4] : split(network_hosts.outputs.subnetResourceId, '/')[4] + } + dependsOn: [ + controlPlane + rgs + ] +} + +module sessionHosts 'modules/sessionHosts/sessionHosts.bicep' = { + name: 'SessionHosts_${timestamp}' + params: { + acceleratedNetworking: management.outputs.validateAcceleratedNetworking + activeDirectorySolution: activeDirectorySolution + artifactsUri: artifactsUri + artifactsUserAssignedIdentityClientId: management.outputs.artifactsUserAssignedIdentityClientId + artifactsUserAssignedIdentityResourceId: management.outputs.artifactsUserAssignedIdentityResourceId + automationAccountName: resourceNames.outputs.automationAccountName + availability: availability + availabilitySetNamePrefix: resourceNames.outputs.availabilitySetNamePrefix + availabilitySetsCount: logic.outputs.availabilitySetsCount + availabilitySetsIndex: logic.outputs.beginAvSetRange + availabilityZones: management.outputs.validateAvailabilityZones + avdAgentBootLoaderMsiName: avdAgentBootLoaderMsiName + avdAgentMsiName: avdAgentMsiName + dataCollectionRuleAssociationName: resourceNames.outputs.dataCollectionRuleAssociationName + dataCollectionRuleResourceId: management.outputs.dataCollectionRuleResourceId + deploymentUserAssignedIdentityClientId: management.outputs.deploymentUserAssignedIdentityClientId + diskEncryptionSetResourceId: management.outputs.diskEncryptionSetResourceId + diskNamePrefix: resourceNames.outputs.diskNamePrefix + diskSku: diskSku + divisionRemainderValue: logic.outputs.divisionRemainderValue + domainJoinPassword: domainJoinPassword + domainJoinUserPrincipalName: domainJoinUserPrincipalName + domainName: domainName + drainMode: drainMode + enableRecoveryServices: recoveryServices + enableScalingTool: scalingTool + fslogix: logic.outputs.fslogix + fslogixContainerType: fslogixContainerType + hostPoolName: resourceNames.outputs.hostPoolName + hostPoolType: hostPoolType + hybridRunbookWorkerGroupName: management.outputs.hybridRunbookWorkerGroupName + imageOffer: imageOffer + imagePublisher: imagePublisher + imageSku: imageSku + imageDefinitionResourceId: imageDefinitionResourceId + location: locationVirtualMachines + logAnalyticsWorkspaceName: resourceNames.outputs.logAnalyticsWorkspaceName + managementVirtualMachineName: management.outputs.virtualMachineName + maxResourcesPerTemplateDeployment: logic.outputs.maxResourcesPerTemplateDeployment + monitoring: monitoring + netAppFileShares: logic.outputs.fslogix ? fslogix.outputs.netAppShares : [ + 'None' + ] + networkInterfaceNamePrefix: resourceNames.outputs.networkInterfaceNamePrefix + organizationalUnitPath: organizationalUnitPath + pooledHostPool: logic.outputs.pooledHostPool + recoveryServicesVaultName: resourceNames.outputs.recoveryServicesVaultName + resourceGroupControlPlane: resourceNames.outputs.resourceGroupControlPlane + resourceGroupHosts: resourceNames.outputs.resourceGroupHosts + resourceGroupManagement: resourceNames.outputs.resourceGroupManagement + roleDefinitions: logic.outputs.roleDefinitions + scalingBeginPeakTime: scalingBeginPeakTime + scalingEndPeakTime: scalingEndPeakTime + scalingLimitSecondsToForceLogOffUser: scalingLimitSecondsToForceLogOffUser + scalingMinimumNumberOfRdsh: scalingMinimumNumberOfRdsh + scalingSessionThresholdPerCPU: scalingSessionThresholdPerCPU + securityPrincipalObjectIds: map(securityPrincipals, item => item.objectId) + securityLogAnalyticsWorkspaceResourceId: securityLogAnalyticsWorkspaceResourceId + sessionHostBatchCount: logic.outputs.sessionHostBatchCount + sessionHostIndex: sessionHostIndex + storageAccountPrefix: resourceNames.outputs.storageAccountNamePrefix + storageCount: storageCount + storageIndex: storageIndex + storageService: logic.outputs.storageService + storageSuffix: logic.outputs.storageSuffix + subnet: length(deploymentLocations) == 1 ? split(network_controlPlane.outputs.subnetResourceId, '/')[10] : split(network_hosts.outputs.subnetResourceId, '/')[10] + tags: tags + timeDifference: logic.outputs.timeDifference + timestamp: timestamp + timeZone: logic.outputs.timeZone + virtualMachineMonitoringAgent: virtualMachineMonitoringAgent + virtualMachineNamePrefix: resourceNames.outputs.virtualMachineNamePrefix + virtualMachinePassword: virtualMachinePassword + virtualMachineSize: virtualMachineSize + virtualMachineUsername: virtualMachineUsername + virtualNetwork: length(deploymentLocations) == 1 ? split(network_controlPlane.outputs.subnetResourceId, '/')[8] : split(network_hosts.outputs.subnetResourceId, '/')[8] + virtualNetworkResourceGroup: length(deploymentLocations) == 1 ? split(network_controlPlane.outputs.subnetResourceId, '/')[4] : split(network_hosts.outputs.subnetResourceId, '/')[4] + } + dependsOn: [ + fslogix + rgs + ] +} + +module cleanUp 'modules/cleanUp/cleanUp.bicep' = { + name: 'CleanUp_${timestamp}' + params: { + fslogixStorageService: fslogixStorageService + location: locationVirtualMachines + resourceGroupManagement: resourceNames.outputs.resourceGroupManagement + scalingTool: scalingTool + timestamp: timestamp + userAssignedIdentityClientId: management.outputs.deploymentUserAssignedIdentityClientId + virtualMachineName: management.outputs.virtualMachineName + } + dependsOn: [ + fslogix + sessionHosts + ] +} diff --git a/src/bicep/add-ons/azureVirtualDesktop/solution.json b/src/bicep/add-ons/azureVirtualDesktop/solution.json new file mode 100644 index 000000000..10d44df86 --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/solution.json @@ -0,0 +1,11543 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12484677965010942290" + } + }, + "parameters": { + "activeDirectorySolution": { + "type": "string", + "allowedValues": [ + "ActiveDirectoryDomainServices", + "MicrosoftEntraDomainServices", + "MicrosoftEntraId", + "MicrosoftEntraIdIntuneEnrollment" + ], + "metadata": { + "description": "The service providing domain services for Azure Virtual Desktop. This is needed to properly configure the session hosts and if applicable, the Azure Storage Account." + } + }, + "artifactsContainerName": { + "type": "string", + "metadata": { + "description": "The name of the Azure Blobs container hosting the required artifacts." + } + }, + "artifactsStorageAccountResourceId": { + "type": "string", + "metadata": { + "description": "The resource ID for the storage account hosting the artifacts in Blob storage." + } + }, + "availability": { + "type": "string", + "defaultValue": "AvailabilityZones", + "allowedValues": [ + "AvailabilitySets", + "AvailabilityZones", + "None" + ], + "metadata": { + "description": "The desired availability option when deploying a pooled host pool. The best practice is to deploy to availability zones for the highest resilency and service level agreement." + } + }, + "avdAgentMsiName": { + "type": "string", + "metadata": { + "description": "The blob name of the MSI file for the AVD Agent installer. The file must be hosted in an Azure Blobs container with the other deployment artifacts." + } + }, + "avdAgentBootLoaderMsiName": { + "type": "string", + "metadata": { + "description": "The blob name of the MSI file for the AVD Agent Boot Loader installer. The file must be hosted in an Azure Blobs container with the other deployment artifacts." + } + }, + "avdObjectId": { + "type": "string", + "metadata": { + "description": "The object ID for the Azure Virtual Desktop enterprise application in Microsoft Entra ID. The object ID can found by selecting Microsoft Applications using the Application type filter in the Enterprise Applications blade of Microsoft Entra ID." + } + }, + "azureNetAppFilesSubnetAddressPrefix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The subnet address prefix for the Azure NetApp Files delegated subnet." + } + }, + "azurePowerShellModuleMsiName": { + "type": "string", + "metadata": { + "description": "The blob name of the MSI file for the Azure PowerShell Module installer. The file must be hosted in an Azure Blobs container with the other deployment artifacts." + } + }, + "customRdpProperty": { + "type": "string", + "defaultValue": "audiocapturemode:i:1;camerastoredirect:s:*;use multimon:i:0;drivestoredirect:s:;", + "metadata": { + "description": "The RDP properties to add or remove RDP functionality on the AVD host pool. The string must end with a semi-colon. Settings reference: https://learn.microsoft.com/windows-server/remote/remote-desktop-services/clients/rdp-files" + } + }, + "desktopFriendlyName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The friendly name for the Desktop application in the desktop application group." + } + }, + "disableBgpRoutePropagation": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Disabling BGP route propagation is a route table configuration that prevents the propagation of on-premises routes to network interfaces in the associated subnets." + } + }, + "diskSku": { + "type": "string", + "defaultValue": "Premium_LRS", + "allowedValues": [ + "Standard_LRS", + "StandardSSD_LRS", + "Premium_LRS" + ], + "metadata": { + "description": "The storage SKU for the managed disks on the AVD session hosts. Production deployments should use Premium_LRS." + } + }, + "domainJoinPassword": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "The password for the account to domain join the AVD session hosts." + } + }, + "domainJoinUserPrincipalName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The user principal name for the account to domain join the AVD session hosts." + } + }, + "domainName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the domain that provides ADDS to the AVD session hosts." + } + }, + "drainMode": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "The drain mode option enables drain mode for the sessions hosts in this deployment to prevent users from accessing the hosts until they have been validated." + } + }, + "environmentShortName": { + "type": "string", + "defaultValue": "dev", + "allowedValues": [ + "dev", + "prod", + "test" + ], + "metadata": { + "description": "The short name for the target environment." + } + }, + "fslogixShareSizeInGB": { + "type": "int", + "defaultValue": 100, + "metadata": { + "description": "The file share size(s) in GB for the Fslogix storage solution." + } + }, + "fslogixContainerType": { + "type": "string", + "defaultValue": "ProfileContainer", + "allowedValues": [ + "CloudCacheProfileContainer", + "CloudCacheProfileOfficeContainer", + "ProfileContainer", + "ProfileOfficeContainer" + ], + "metadata": { + "description": "If deploying FSLogix, select the desired type of container for user profiles. https://learn.microsoft.com/en-us/fslogix/concepts-container-types" + } + }, + "fslogixStorageService": { + "type": "string", + "defaultValue": "AzureFiles Standard", + "allowedValues": [ + "AzureNetAppFiles Premium", + "AzureNetAppFiles Standard", + "AzureFiles Premium", + "AzureFiles Standard", + "None" + ], + "metadata": { + "description": "Enable an Fslogix storage option to manage user profiles for the AVD session hosts. The selected service & SKU should provide sufficient IOPS for all of your users. https://docs.microsoft.com/en-us/azure/architecture/example-scenario/wvd/windows-virtual-desktop-fslogix#performance-requirements" + } + }, + "hostPoolPublicNetworkAccess": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled", + "EnabledForClientsOnly", + "EnabledForSessionHostsOnly" + ], + "metadata": { + "description": "The type of public network access for the host pool." + } + }, + "hostPoolType": { + "type": "string", + "defaultValue": "Pooled DepthFirst", + "allowedValues": [ + "Pooled DepthFirst", + "Pooled BreadthFirst", + "Personal Automatic", + "Personal Direct" + ], + "metadata": { + "description": "These options specify the host pool type and depending on the type provides the load balancing options and assignment types." + } + }, + "hubAzureFirewallResourceId": { + "type": "string", + "metadata": { + "description": "The resource ID for the Azure Firewall in the HUB subscription" + } + }, + "hubSubnetResourceId": { + "type": "string", + "metadata": { + "description": "The resource ID for the subnet in the Shared Services subscription. This is required for the private endpoint on the AVD Global Workspace." + } + }, + "hubVirtualNetworkResourceId": { + "type": "string", + "metadata": { + "description": "The resource ID for the Azure Virtual Network in the HUB subscription." + } + }, + "identifier": { + "type": "string", + "defaultValue": "avd", + "maxLength": 3, + "metadata": { + "description": "The unique identifier between each business unit or project supporting AVD in your tenant. This is the unique naming component between each AVD stamp." + } + }, + "imageDefinitionResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The resource ID for the Compute Gallery Image Version. Do not set this value if using a marketplace image." + } + }, + "imageOffer": { + "type": "string", + "defaultValue": "office-365", + "metadata": { + "description": "Offer for the virtual machine image" + } + }, + "imagePublisher": { + "type": "string", + "defaultValue": "MicrosoftWindowsDesktop", + "metadata": { + "description": "Publisher for the virtual machine image" + } + }, + "imageSku": { + "type": "string", + "defaultValue": "win11-22h2-avd-m365", + "metadata": { + "description": "SKU for the virtual machine image" + } + }, + "locationControlPlane": { + "type": "string", + "defaultValue": "[deployment().location]", + "metadata": { + "description": "The deployment location for the AVD management resources." + } + }, + "locationVirtualMachines": { + "type": "string", + "defaultValue": "[deployment().location]", + "metadata": { + "description": "The deployment location for the AVD sessions hosts." + } + }, + "logAnalyticsWorkspaceRetention": { + "type": "int", + "defaultValue": 30, + "minValue": 30, + "maxValue": 730, + "metadata": { + "description": "The retention for the Log Analytics Workspace to setup the AVD monitoring solution" + } + }, + "logAnalyticsWorkspaceSku": { + "type": "string", + "defaultValue": "PerGB2018", + "allowedValues": [ + "Free", + "Standard", + "Premium", + "PerNode", + "PerGB2018", + "Standalone", + "CapacityReservation" + ], + "metadata": { + "description": "The SKU for the Log Analytics Workspace to setup the AVD monitoring solution" + } + }, + "maxSessionLimit": { + "type": "int", + "metadata": { + "description": "The maximum number of sessions per AVD session host." + } + }, + "monitoring": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Deploys the required monitoring resources to enable AVD Insights and monitor features in the automation account." + } + }, + "organizationalUnitPath": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The distinguished name for the target Organization Unit in Active Directory Domain Services." + } + }, + "recoveryServices": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable backups to an Azure Recovery Services vault. For a pooled host pool this will enable backups on the Azure file share. For a personal host pool this will enable backups on the AVD sessions hosts." + } + }, + "scalingBeginPeakTime": { + "type": "string", + "defaultValue": "9:00", + "metadata": { + "description": "The time when session hosts will scale up and continue to stay on to support peak demand; Format 24 hours e.g. 9:00 for 9am" + } + }, + "scalingEndPeakTime": { + "type": "string", + "defaultValue": "17:00", + "metadata": { + "description": "The time when session hosts will scale down and stay off to support low demand; Format 24 hours e.g. 17:00 for 5pm" + } + }, + "scalingLimitSecondsToForceLogOffUser": { + "type": "string", + "defaultValue": "0", + "metadata": { + "description": "The number of seconds to wait before automatically signing out users. If set to 0 any session host that has user sessions will be left untouched" + } + }, + "scalingMinimumNumberOfRdsh": { + "type": "string", + "defaultValue": "0", + "metadata": { + "description": "The minimum number of session host VMs to keep running during off-peak hours. The scaling tool will not work if all virtual machines are turned off and the Start VM On Connect solution is not enabled." + } + }, + "scalingSessionThresholdPerCPU": { + "type": "string", + "defaultValue": "1", + "metadata": { + "description": "The maximum number of sessions per CPU that will be used as a threshold to determine when new session host VMs need to be started during peak hours" + } + }, + "scalingTool": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Deploys the required resources for the Scaling Tool. https://docs.microsoft.com/en-us/azure/virtual-desktop/scaling-automation-logic-apps" + } + }, + "securityLogAnalyticsWorkspaceResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The resource ID of the log analytics workspace used for Azure Sentinel and / or Defender for Cloud. When using the Microsoft monitoring Agent, this allows you to multihome the agent to reduce unnecessary log collection and reduce cost." + } + }, + "securityPrincipals": { + "type": "array", + "metadata": { + "description": "The array of Security Principals with their object IDs and display names to assign to the AVD Application Group and FSLogix Storage." + } + }, + "sessionHostCount": { + "type": "int", + "defaultValue": 1, + "minValue": 0, + "maxValue": 5000, + "metadata": { + "description": "The number of session hosts to deploy in the host pool. Ensure you have the approved quota to deploy the desired count." + } + }, + "sessionHostIndex": { + "type": "int", + "defaultValue": 0, + "minValue": 0, + "maxValue": 4999, + "metadata": { + "description": "The starting number for the session hosts. This is important when adding virtual machines to ensure an update deployment is not performed on an existing, active session host." + } + }, + "stampIndex": { + "type": "int", + "defaultValue": 0, + "minValue": 0, + "maxValue": 9, + "metadata": { + "description": "The stamp index allows for multiple AVD stamps with the same business unit or project to support different use cases. For example, \"0\" could be used for an office workers host pool and \"1\" could be used for a developers host pool within the \"finance\" business unit." + } + }, + "storageCount": { + "type": "int", + "defaultValue": 1, + "minValue": 0, + "maxValue": 100, + "metadata": { + "description": "The number of storage accounts to deploy to support sharding across multiple storage accounts. https://docs.microsoft.com/en-us/azure/architecture/patterns/sharding" + } + }, + "storageIndex": { + "type": "int", + "defaultValue": 0, + "minValue": 0, + "maxValue": 99, + "metadata": { + "description": "The starting number for the names of the storage accounts to support sharding across multiple storage accounts. https://docs.microsoft.com/en-us/azure/architecture/patterns/sharding" + } + }, + "subnetAddressPrefixes": { + "type": "array", + "metadata": { + "description": "The address prefix(es) for the new subnet(s) that will be created in the spoke virtual network(s)." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "The Key / value pairs of metadata for the Azure resource groups and resources." + } + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]", + "metadata": { + "description": "DO NOT MODIFY THIS VALUE! The timestamp is needed to differentiate deployments for certain Azure resources and must be set using a parameter." + } + }, + "validationEnvironment": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "The validation environment setting on the AVD host pool determines whether the hostpool should receive AVD preview features for testing." + } + }, + "virtualMachineMonitoringAgent": { + "type": "string", + "defaultValue": "LogAnalyticsAgent", + "allowedValues": [ + "AzureMonitorAgent", + "LogAnalyticsAgent" + ], + "metadata": { + "description": "Input the desired monitoring agent to send events and performance counters to a log analytics workspace." + } + }, + "virtualMachinePassword": { + "type": "securestring", + "metadata": { + "description": "The local administrator password for the AVD session hosts" + } + }, + "virtualMachineSize": { + "type": "string", + "defaultValue": "Standard_D4ads_v5", + "metadata": { + "description": "The virtual machine SKU for the AVD session hosts." + } + }, + "virtualMachineUsername": { + "type": "string", + "metadata": { + "description": "The local administrator username for the AVD session hosts" + } + }, + "virtualNetworkAddressPrefixes": { + "type": "array", + "metadata": { + "description": "The address prefix for the new spoke virtual network(s)." + } + }, + "workspaceFriendlyName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The friendly name for the AVD workspace that is displayed in the end-user client." + } + }, + "workspacePublicNetworkAccess": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "The public network access setting on the AVD workspace either disables public network access or allows both public and private network access." + } + } + }, + "variables": { + "artifactsUri": "[format('https://{0}.blob.{1}/{2}/', variables('artifactsStorageAccountName'), environment().suffixes.storage, parameters('artifactsContainerName'))]", + "artifactsStorageAccountName": "[split(parameters('artifactsStorageAccountResourceId'), '/')[8]]", + "privateDnsZoneResourceIdPrefix": "[format('/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Network/privateDnsZones/', split(parameters('hubVirtualNetworkResourceId'), '/')[2], split(parameters('hubVirtualNetworkResourceId'), '/')[4])]", + "deploymentLocations": "[union(createArray(parameters('locationControlPlane')), createArray(parameters('locationVirtualMachines')))]", + "resourceGroupsCount": "[add(add(4, length(variables('deploymentLocations'))), if(equals(parameters('fslogixStorageService'), 'None'), 0, 1))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('ResourceNames_{0}', parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "environmentShortName": { + "value": "[parameters('environmentShortName')]" + }, + "identifier": { + "value": "[parameters('identifier')]" + }, + "locationControlPlane": { + "value": "[parameters('locationControlPlane')]" + }, + "locationVirtualMachines": { + "value": "[parameters('locationVirtualMachines')]" + }, + "stampIndex": { + "value": "[parameters('stampIndex')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "10484903410146163368" + } + }, + "parameters": { + "environmentShortName": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "locationControlPlane": { + "type": "string" + }, + "locationVirtualMachines": { + "type": "string" + }, + "stampIndex": { + "type": "int" + } + }, + "variables": { + "$fxv#0": { + "AzureChina": { + "chinaeast": { + "abbreviation": "cne", + "recoveryServicesGeo": "sha", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + }, + "chinaeast2": { + "abbreviation": "cne2", + "recoveryServicesGeo": "sha2", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + }, + "chinanorth": { + "abbreviation": "cnn", + "recoveryServicesGeo": "bjb", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + }, + "chinanorth2": { + "abbreviation": "cnn2", + "recoveryServicesGeo": "bjb2", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + }, + "chinanorth3": { + "abbreviation": "cnn3", + "recoveryServicesGeo": "", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + } + }, + "AzureCloud": { + "australiacentral": { + "abbreviation": "auc", + "recoveryServicesGeo": "acl", + "timeDifference": "+10:00", + "timeZone": "AUS Eastern Standard Time" + }, + "australiacentral2": { + "abbreviation": "auc2", + "recoveryServicesGeo": "acl2", + "timeDifference": "+10:00", + "timeZone": "AUS Eastern Standard Time" + }, + "australiaeast": { + "abbreviation": "aue", + "recoveryServicesGeo": "ae", + "timeDifference": "+10:00", + "timeZone": "AUS Eastern Standard Time" + }, + "australiasoutheast": { + "abbreviation": "ause", + "recoveryServicesGeo": "ase", + "timeDifference": "+10:00", + "timeZone": "AUS Eastern Standard Time" + }, + "brazilsouth": { + "abbreviation": "brs", + "recoveryServicesGeo": "brs", + "timeDifference": "-3:00", + "timeZone": "E. South America Standard Time" + }, + "brazilsoutheast": { + "abbreviation": "brse", + "recoveryServicesGeo": "bse", + "timeDifference": "-3:00", + "timeZone": "E. South America Standard Time" + }, + "canadacentral": { + "abbreviation": "cac", + "recoveryServicesGeo": "cnc", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "canadaeast": { + "abbreviation": "cae", + "recoveryServicesGeo": "cne", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "centralindia": { + "abbreviation": "inc", + "recoveryServicesGeo": "inc", + "timeDifference": "+5:30", + "timeZone": "India Standard Time" + }, + "centralus": { + "abbreviation": "usc", + "recoveryServicesGeo": "cus", + "timeDifference": "-6:00", + "timeZone": "Central Standard Time" + }, + "eastasia": { + "abbreviation": "ase", + "recoveryServicesGeo": "ea", + "timeDifference": "+8:00", + "timeZone": "China Standard Time" + }, + "eastus": { + "abbreviation": "use", + "recoveryServicesGeo": "eus", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "eastus2": { + "abbreviation": "use2", + "recoveryServicesGeo": "eus2", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "francecentral": { + "abbreviation": "frc", + "recoveryServicesGeo": "frc", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "francesouth": { + "abbreviation": "frs", + "recoveryServicesGeo": "frs", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "germanynorth": { + "abbreviation": "den", + "recoveryServicesGeo": "gn", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "germanywestcentral": { + "abbreviation": "dewc", + "recoveryServicesGeo": "gwc", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "israelcentral": { + "abbreviation": "ilc", + "recoveryServicesGeo": "ilc", + "timeDifference": "+2:00", + "timeZone": "Israel Standard Time" + }, + "italynorth": { + "abbreviation": "itn", + "recoveryServicesGeo": "itn", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "japaneast": { + "abbreviation": "jpe", + "recoveryServicesGeo": "jpe", + "timeDifference": "+9:00", + "timeZone": "Tokyo Standard Time" + }, + "japanwest": { + "abbreviation": "jpw", + "recoveryServicesGeo": "jpw", + "timeDifference": "+9:00", + "timeZone": "Tokyo Standard Time" + }, + "jioindiacentral": { + "abbreviation": "injc", + "recoveryServicesGeo": "jic", + "timeDifference": "+5:30", + "timeZone": "India Standard Time" + }, + "jioindiawest": { + "abbreviation": "injw", + "recoveryServicesGeo": "jiw", + "timeDifference": "+5:30", + "timeZone": "India Standard Time" + }, + "koreacentral": { + "abbreviation": "krc", + "recoveryServicesGeo": "krc", + "timeDifference": "+9:00", + "timeZone": "Korea Standard Time" + }, + "koreasouth": { + "abbreviation": "krs", + "recoveryServicesGeo": "krs", + "timeDifference": "+9:00", + "timeZone": "Korea Standard Time" + }, + "newzealandnorth": { + "abbreviation": "nzn", + "recoveryServicesGeo": "", + "timeDifference": "+13:00", + "timeZone": "New Zealand Standard Time" + }, + "northcentralus": { + "abbreviation": "usnc", + "recoveryServicesGeo": "ncus", + "timeDifference": "-6:00", + "timeZone": "Central Standard Time" + }, + "northeurope": { + "abbreviation": "eun", + "recoveryServicesGeo": "ne", + "timeDifference": "0:00", + "timeZone": "GMT Standard Time" + }, + "norwayeast": { + "abbreviation": "noe", + "recoveryServicesGeo": "nwe", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "norwaywest": { + "abbreviation": "now", + "recoveryServicesGeo": "nww", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "polandcentral": { + "abbreviation": "plc", + "recoveryServicesGeo": "plc", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "qatarcentral": { + "abbreviation": "qac", + "recoveryServicesGeo": "qac", + "timeDifference": "+3:00", + "timeZone": "Arabian Standard Time" + }, + "southafricanorth": { + "abbreviation": "zan", + "recoveryServicesGeo": "san", + "timeDifference": "+2:00", + "timeZone": "South Africa Standard Time" + }, + "southafricawest": { + "abbreviation": "zaw", + "recoveryServicesGeo": "saw", + "timeDifference": "+2:00", + "timeZone": "South Africa Standard Time" + }, + "southcentralus": { + "abbreviation": "ussc", + "recoveryServicesGeo": "scus", + "timeDifference": "-6:00", + "timeZone": "Central Standard Time" + }, + "southeastasia": { + "abbreviation": "asse", + "recoveryServicesGeo": "sea", + "timeDifference": "+8:00", + "timeZone": "Singapore Standard Time" + }, + "southindia": { + "abbreviation": "ins", + "recoveryServicesGeo": "ins", + "timeDifference": "+5:30", + "timeZone": "India Standard Time" + }, + "swedencentral": { + "abbreviation": "sec", + "recoveryServicesGeo": "sdc", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "switzerlandnorth": { + "abbreviation": "chn", + "recoveryServicesGeo": "szn", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "switzerlandwest": { + "abbreviation": "chw", + "recoveryServicesGeo": "szw", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "uaecentral": { + "abbreviation": "aec", + "recoveryServicesGeo": "uac", + "timeDifference": "+3:00", + "timeZone": "Arabian Standard Time" + }, + "uaenorth": { + "abbreviation": "aen", + "recoveryServicesGeo": "uan", + "timeDifference": "+3:00", + "timeZone": "Arabian Standard Time" + }, + "uksouth": { + "abbreviation": "uks", + "recoveryServicesGeo": "uks", + "timeDifference": "0:00", + "timeZone": "GMT Standard Time" + }, + "ukwest": { + "abbreviation": "ukw", + "recoveryServicesGeo": "ukw", + "timeDifference": "0:00", + "timeZone": "GMT Standard Time" + }, + "westcentralus": { + "abbreviation": "uswc", + "recoveryServicesGeo": "wcus", + "timeDifference": "-7:00", + "timeZone": "Mountain Standard Time" + }, + "westeurope": { + "abbreviation": "euw", + "recoveryServicesGeo": "we", + "timeDifference": "+1:00", + "timeZone": "Central Europe Standard Time" + }, + "westindia": { + "abbreviation": "inw", + "recoveryServicesGeo": "inw", + "timeDifference": "+5:30", + "timeZone": "India Standard Time" + }, + "westus": { + "abbreviation": "usw", + "recoveryServicesGeo": "wus", + "timeDifference": "-8:00", + "timeZone": "Pacific Standard Time" + }, + "westus2": { + "abbreviation": "usw2", + "recoveryServicesGeo": "wus2", + "timeDifference": "-8:00", + "timeZone": "Pacific Standard Time" + }, + "westus3": { + "abbreviation": "usw3", + "recoveryServicesGeo": "wus3", + "timeDifference": "-7:00", + "timeZone": "Mountain Standard Time" + } + }, + "AzureUSGovernment": { + "usdodcentral": { + "abbreviation": "dodc", + "recoveryServicesGeo": "udc", + "timeDifference": "-6:00", + "timeZone": "Central Standard Time" + }, + "usdodeast": { + "abbreviation": "dode", + "recoveryServicesGeo": "ude", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "usgovarizona": { + "abbreviation": "az", + "recoveryServicesGeo": "uga", + "timeDifference": "-7:00", + "timeZone": "Mountain Standard Time" + }, + "usgovtexas": { + "abbreviation": "tx", + "recoveryServicesGeo": "ugt", + "timeDifference": "-6:00", + "timeZone": "Central Standard Time" + }, + "usgovvirginia": { + "abbreviation": "va", + "recoveryServicesGeo": "ugv", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + } + }, + "USNat": { + "usnateast": { + "abbreviation": "east", + "recoveryServicesGeo": "exe", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "usnatwest": { + "abbreviation": "west", + "recoveryServicesGeo": "exw", + "timeDifference": "-8:00", + "timeZone": "Pacific Standard Time" + } + }, + "USSec": { + "usseceast": { + "abbreviation": "east", + "recoveryServicesGeo": "rxe", + "timeDifference": "-5:00", + "timeZone": "Eastern Standard Time" + }, + "ussecwest": { + "abbreviation": "west", + "recoveryServicesGeo": "rxw", + "timeDifference": "-8:00", + "timeZone": "Pacific Standard Time" + } + } + }, + "$fxv#1": { + "automationAccounts": "aa", + "availabilitySets": "as", + "dataCollectionRuleAssociations": "dcra", + "dataCollectionRules": "dcr", + "desktopApplicationGroups": "dag", + "diskAccesses": "da", + "remoteApplicationGroups": "rag", + "disks": "disk", + "diskEncryptionSets": "des", + "hostPools": "hp", + "keyVaults": "kv", + "logAnalyticsWorkspaces": "law", + "netAppAccounts": "naa", + "netAppCapacityPools": "nacp", + "networkInterfaces": "nic", + "networkSecurityGroups": "nsg", + "recoveryServicesVaults": "rsv", + "resourceGroups": "rg", + "routeTables": "rt", + "storageAccounts": "sa", + "userAssignedIdentities": "uai", + "virtualMachines": "vm", + "virtualNetworks": "vnet", + "workspaces": "ws" + }, + "namingConvention": "[format('{0}-{1}-resourceType-{2}-location', parameters('identifier'), parameters('stampIndex'), parameters('environmentShortName'))]", + "namingConvention_Global": "[format('resourceType-{0}-location', parameters('environmentShortName'))]", + "namingConvention_Shared": "[format('{0}-resourceType-{1}-location', parameters('identifier'), parameters('environmentShortName'))]", + "cloudEndpointSuffix": "[replace(replace(environment().resourceManager, 'https://management.', ''), '/', '')]", + "privateDnsZoneSuffixes_AzureAutomation": { + "AzureCloud": "net", + "AzureUSGovernment": "us" + }, + "privateDnsZoneSuffixes_AzureVirtualDesktop": { + "AzureCloud": "microsoft.com", + "AzureUSGovernment": "azure.us" + }, + "privateDnsZoneSuffixes_Backup": { + "AzureCloud": "windowsazure.com", + "AzureUSGovernment": "windowsazure.us" + }, + "privateDnsZoneSuffixes_Monitor": { + "AzureCloud": "azure.com", + "AzureUSGovernment": "azure.us" + }, + "locations": "[variables('$fxv#0')[environment().name]]", + "resourceAbbreviations": "[variables('$fxv#1')]", + "agentSvcPrivateDnsZoneName": "[format('privatelink.agentsvc.azure-automation.{0}', coalesce(variables('privateDnsZoneSuffixes_AzureAutomation')[environment().name], variables('cloudEndpointSuffix')))]", + "automationAccountName": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').automationAccounts), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]", + "availabilitySetNamePrefix": "[format('{0}-', replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').availabilitySets), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation))]", + "avdGlobalPrivateDnsZoneName": "[format('privatelink-global.wvd.{0}', coalesce(variables('privateDnsZoneSuffixes_AzureVirtualDesktop')[environment().name], variables('cloudEndpointSuffix')))]", + "avdPrivateDnsZoneName": "[format('privatelink.wvd.{0}', coalesce(variables('privateDnsZoneSuffixes_AzureVirtualDesktop')[environment().name], variables('cloudEndpointSuffix')))]", + "azureAutomationPrivateDnsZoneName": "[format('privatelink.azure-automation.{0}', coalesce(variables('privateDnsZoneSuffixes_AzureAutomation')[environment().name], variables('cloudEndpointSuffix')))]", + "backupPrivateDnsZoneName": "[format('privatelink.{0}.backup.{1}', variables('locations')[parameters('locationVirtualMachines')].recoveryServicesGeo, coalesce(variables('privateDnsZoneSuffixes_Backup')[environment().name], variables('cloudEndpointSuffix')))]", + "blobPrivateDnsZoneName": "[format('privatelink.blob.{0}', environment().suffixes.storage)]", + "dataCollectionRuleAssociationName": "[format('{0}-avdi', replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').dataCollectionRuleAssociations), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation))]", + "dataCollectionRuleName": "[format('microsoft-avdi-{0}', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]", + "desktopApplicationGroupName": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').desktopApplicationGroups), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation)]", + "diskAccessName": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').diskAccesses), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]", + "diskEncryptionSetName": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').diskEncryptionSets), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]", + "diskNamePrefix": "[format('{0}-', replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').disks), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation))]", + "filePrivateDnsZoneName": "[format('privatelink.file.{0}', environment().suffixes.storage)]", + "fileShareNames": { + "CloudCacheProfileContainer": [ + "profile-containers" + ], + "CloudCacheProfileOfficeContainer": [ + "office-containers", + "profile-containers" + ], + "ProfileContainer": [ + "profile-containers" + ], + "ProfileOfficeContainer": [ + "office-containers", + "profile-containers" + ] + }, + "hostPoolName": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').hostPools), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation)]", + "keyVaultName": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').keyVaults), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]", + "keyVaultPrivateDnsZoneName": "[replace(format('privatelink{0}', environment().suffixes.keyvaultDns), 'vault', 'vaultcore')]", + "logAnalyticsWorkspaceName": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').logAnalyticsWorkspaces), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]", + "netAppAccountName": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').netAppAccounts), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]", + "netAppCapacityPoolName": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').netAppCapacityPools), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]", + "networkInterfaceNamePrefix": "[format('{0}-', replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').networkInterfaces), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation))]", + "networkSecurityGroupNames": [ + "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').networkSecurityGroups), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation)]", + "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').networkSecurityGroups), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]" + ], + "monitorPrivateDnsZoneName": "[format('privatelink.monitor.{0}', coalesce(variables('privateDnsZoneSuffixes_Monitor')[environment().name], variables('cloudEndpointSuffix')))]", + "odsOpinsightsPrivateDnsZoneName": "[format('privatelink.ods.opinsights.{0}', coalesce(variables('privateDnsZoneSuffixes_Monitor')[environment().name], variables('cloudEndpointSuffix')))]", + "omsOpinsightsPrivateDnsZoneName": "[format('privatelink.oms.opinsights.{0}', coalesce(variables('privateDnsZoneSuffixes_Monitor')[environment().name], variables('cloudEndpointSuffix')))]", + "queuePrivateDnsZoneName": "[format('privatelink.queue.{0}', environment().suffixes.storage)]", + "recoveryServicesVaultName": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').recoveryServicesVaults), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]", + "resourceGroupControlPlane": "[format('{0}-avd-controlPlane', replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').resourceGroups), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation))]", + "resourceGroupFeedWorkspace": "[format('{0}-avd-feedWorkspace', replace(replace(variables('namingConvention_Shared'), 'resourceType', variables('resourceAbbreviations').resourceGroups), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation))]", + "resourceGroupGlobalWorkspace": "[format('{0}-avd-globalWorkspace', replace(replace(variables('namingConvention_Global'), 'resourceType', variables('resourceAbbreviations').resourceGroups), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation))]", + "resourceGroupHosts": "[format('{0}-avd-sessionHosts', replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').resourceGroups), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation))]", + "resourceGroupManagement": "[format('{0}-avd-management', replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').resourceGroups), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation))]", + "resourceGroupsNetwork": [ + "[format('{0}-avd-network', replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').resourceGroups), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation))]", + "[format('{0}-avd-network', replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').resourceGroups), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation))]" + ], + "resourceGroupStorage": "[format('{0}-avd-profileStorage', replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').resourceGroups), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation))]", + "routeTables": [ + "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').routeTables), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation)]", + "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').routeTables), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]" + ], + "storageAccountNamePrefix": "[replace(replace(replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').storageAccounts), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation), parameters('environmentShortName'), first(parameters('environmentShortName'))), '-', '')]", + "userAssignedIdentityNamePrefix": "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').userAssignedIdentities), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]", + "virtualMachineNamePrefix": "[replace(replace(replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').virtualMachines), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation), parameters('environmentShortName'), first(parameters('environmentShortName'))), '-', '')]", + "virtualNetworkNames": [ + "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').virtualNetworks), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation)]", + "[replace(replace(variables('namingConvention'), 'resourceType', variables('resourceAbbreviations').virtualNetworks), 'location', variables('locations')[parameters('locationVirtualMachines')].abbreviation)]" + ], + "workspaceFeedNamePrefix": "[replace(replace(variables('namingConvention_Shared'), 'resourceType', variables('resourceAbbreviations').workspaces), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation)]", + "workspaceGlobalNamePrefix": "[replace(replace(variables('namingConvention_Global'), 'resourceType', variables('resourceAbbreviations').workspaces), 'location', variables('locations')[parameters('locationControlPlane')].abbreviation)]" + }, + "resources": [], + "outputs": { + "agentSvcPrivateDnsZoneName": { + "type": "string", + "value": "[variables('agentSvcPrivateDnsZoneName')]" + }, + "automationAccountName": { + "type": "string", + "value": "[variables('automationAccountName')]" + }, + "availabilitySetNamePrefix": { + "type": "string", + "value": "[variables('availabilitySetNamePrefix')]" + }, + "avdGlobalPrivateDnsZoneName": { + "type": "string", + "value": "[variables('avdGlobalPrivateDnsZoneName')]" + }, + "avdPrivateDnsZoneName": { + "type": "string", + "value": "[variables('avdPrivateDnsZoneName')]" + }, + "azureAutomationPrivateDnsZoneName": { + "type": "string", + "value": "[variables('azureAutomationPrivateDnsZoneName')]" + }, + "backupPrivateDnsZoneName": { + "type": "string", + "value": "[variables('backupPrivateDnsZoneName')]" + }, + "blobPrivateDnsZoneName": { + "type": "string", + "value": "[variables('blobPrivateDnsZoneName')]" + }, + "dataCollectionRuleAssociationName": { + "type": "string", + "value": "[variables('dataCollectionRuleAssociationName')]" + }, + "dataCollectionRuleName": { + "type": "string", + "value": "[variables('dataCollectionRuleName')]" + }, + "desktopApplicationGroupName": { + "type": "string", + "value": "[variables('desktopApplicationGroupName')]" + }, + "diskAccessName": { + "type": "string", + "value": "[variables('diskAccessName')]" + }, + "diskEncryptionSetName": { + "type": "string", + "value": "[variables('diskEncryptionSetName')]" + }, + "diskNamePrefix": { + "type": "string", + "value": "[variables('diskNamePrefix')]" + }, + "filePrivateDnsZoneName": { + "type": "string", + "value": "[variables('filePrivateDnsZoneName')]" + }, + "fileShareNames": { + "type": "object", + "value": "[variables('fileShareNames')]" + }, + "hostPoolName": { + "type": "string", + "value": "[variables('hostPoolName')]" + }, + "keyVaultName": { + "type": "string", + "value": "[variables('keyVaultName')]" + }, + "keyVaultPrivateDnsZoneName": { + "type": "string", + "value": "[variables('keyVaultPrivateDnsZoneName')]" + }, + "locations": { + "type": "object", + "value": "[variables('locations')]" + }, + "logAnalyticsWorkspaceName": { + "type": "string", + "value": "[variables('logAnalyticsWorkspaceName')]" + }, + "monitorPrivateDnsZoneName": { + "type": "string", + "value": "[variables('monitorPrivateDnsZoneName')]" + }, + "odsOpinsightsPrivateDnsZoneName": { + "type": "string", + "value": "[variables('odsOpinsightsPrivateDnsZoneName')]" + }, + "omsOpinsightsPrivateDnsZoneName": { + "type": "string", + "value": "[variables('omsOpinsightsPrivateDnsZoneName')]" + }, + "netAppAccountName": { + "type": "string", + "value": "[variables('netAppAccountName')]" + }, + "netAppCapacityPoolName": { + "type": "string", + "value": "[variables('netAppCapacityPoolName')]" + }, + "networkInterfaceNamePrefix": { + "type": "string", + "value": "[variables('networkInterfaceNamePrefix')]" + }, + "networkSecurityGroupNames": { + "type": "array", + "value": "[variables('networkSecurityGroupNames')]" + }, + "queuePrivateDnsZoneName": { + "type": "string", + "value": "[variables('queuePrivateDnsZoneName')]" + }, + "recoveryServicesVaultName": { + "type": "string", + "value": "[variables('recoveryServicesVaultName')]" + }, + "resourceAbbreviations": { + "type": "object", + "value": "[variables('resourceAbbreviations')]" + }, + "resourceGroupControlPlane": { + "type": "string", + "value": "[variables('resourceGroupControlPlane')]" + }, + "resourceGroupFeedWorkspace": { + "type": "string", + "value": "[variables('resourceGroupFeedWorkspace')]" + }, + "resourceGroupGlobalWorkspace": { + "type": "string", + "value": "[variables('resourceGroupGlobalWorkspace')]" + }, + "resourceGroupHosts": { + "type": "string", + "value": "[variables('resourceGroupHosts')]" + }, + "resourceGroupManagement": { + "type": "string", + "value": "[variables('resourceGroupManagement')]" + }, + "resourceGroupsNetwork": { + "type": "array", + "value": "[variables('resourceGroupsNetwork')]" + }, + "resourceGroupStorage": { + "type": "string", + "value": "[variables('resourceGroupStorage')]" + }, + "routeTables": { + "type": "array", + "value": "[variables('routeTables')]" + }, + "storageAccountNamePrefix": { + "type": "string", + "value": "[variables('storageAccountNamePrefix')]" + }, + "userAssignedIdentityNamePrefix": { + "type": "string", + "value": "[variables('userAssignedIdentityNamePrefix')]" + }, + "virtualMachineNamePrefix": { + "type": "string", + "value": "[variables('virtualMachineNamePrefix')]" + }, + "virtulNetworkNames": { + "type": "array", + "value": "[variables('virtualNetworkNames')]" + }, + "workspaceFeedNamePrefix": { + "type": "string", + "value": "[variables('workspaceFeedNamePrefix')]" + }, + "workspaceGlobalNamePrefix": { + "type": "string", + "value": "[variables('workspaceGlobalNamePrefix')]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Logic_{0}', parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "activeDirectorySolution": { + "value": "[parameters('activeDirectorySolution')]" + }, + "deploymentLocations": { + "value": "[variables('deploymentLocations')]" + }, + "diskSku": { + "value": "[parameters('diskSku')]" + }, + "domainName": { + "value": "[parameters('domainName')]" + }, + "fileShareNames": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.fileShareNames.value]" + }, + "fslogixContainerType": { + "value": "[parameters('fslogixContainerType')]" + }, + "fslogixStorageService": { + "value": "[parameters('fslogixStorageService')]" + }, + "hostPoolType": { + "value": "[parameters('hostPoolType')]" + }, + "imageOffer": { + "value": "[parameters('imageOffer')]" + }, + "imagePublisher": { + "value": "[parameters('imagePublisher')]" + }, + "imageSku": { + "value": "[parameters('imageSku')]" + }, + "locations": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.locations.value]" + }, + "locationVirtualMachines": { + "value": "[parameters('locationVirtualMachines')]" + }, + "resourceGroupControlPlane": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupControlPlane.value]" + }, + "resourceGroupFeedWorkspace": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupFeedWorkspace.value]" + }, + "resourceGroupHosts": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupHosts.value]" + }, + "resourceGroupManagement": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupManagement.value]" + }, + "resourceGroupsNetwork": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupsNetwork.value]" + }, + "resourceGroupStorage": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupStorage.value]" + }, + "securityPrincipals": { + "value": "[parameters('securityPrincipals')]" + }, + "sessionHostCount": { + "value": "[parameters('sessionHostCount')]" + }, + "sessionHostIndex": { + "value": "[parameters('sessionHostIndex')]" + }, + "virtualMachineNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.virtualMachineNamePrefix.value]" + }, + "virtualMachineSize": { + "value": "[parameters('virtualMachineSize')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "11063598142351252317" + } + }, + "parameters": { + "activeDirectorySolution": { + "type": "string" + }, + "deploymentLocations": { + "type": "array" + }, + "diskSku": { + "type": "string" + }, + "domainName": { + "type": "string" + }, + "fileShareNames": { + "type": "object" + }, + "fslogixContainerType": { + "type": "string" + }, + "fslogixStorageService": { + "type": "string" + }, + "hostPoolType": { + "type": "string" + }, + "imageOffer": { + "type": "string" + }, + "imagePublisher": { + "type": "string" + }, + "imageSku": { + "type": "string" + }, + "locations": { + "type": "object" + }, + "locationVirtualMachines": { + "type": "string" + }, + "resourceGroupControlPlane": { + "type": "string" + }, + "resourceGroupFeedWorkspace": { + "type": "string" + }, + "resourceGroupHosts": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "resourceGroupsNetwork": { + "type": "array" + }, + "resourceGroupStorage": { + "type": "string" + }, + "securityPrincipals": { + "type": "array" + }, + "sessionHostCount": { + "type": "int" + }, + "sessionHostIndex": { + "type": "int" + }, + "virtualMachineNamePrefix": { + "type": "string" + }, + "virtualMachineSize": { + "type": "string" + } + }, + "variables": { + "maxResourcesPerTemplateDeployment": 88, + "divisionValue": "[div(parameters('sessionHostCount'), variables('maxResourcesPerTemplateDeployment'))]", + "divisionRemainderValue": "[mod(parameters('sessionHostCount'), variables('maxResourcesPerTemplateDeployment'))]", + "sessionHostBatchCount": "[if(greater(variables('divisionRemainderValue'), 0), add(variables('divisionValue'), 1), variables('divisionValue'))]", + "maxAvSetMembers": 200, + "beginAvSetRange": "[div(parameters('sessionHostIndex'), variables('maxAvSetMembers'))]", + "endAvSetRange": "[div(add(parameters('sessionHostCount'), parameters('sessionHostIndex')), variables('maxAvSetMembers'))]", + "availabilitySetsCount": "[length(range(variables('beginAvSetRange'), add(sub(variables('endAvSetRange'), variables('beginAvSetRange')), 1)))]", + "fileShares": "[parameters('fileShareNames')[parameters('fslogixContainerType')]]", + "fslogix": "[if(or(equals(parameters('fslogixStorageService'), 'None'), not(contains(parameters('activeDirectorySolution'), 'DomainServices'))), false(), true())]", + "netbios": "[split(parameters('domainName'), '.')[0]]", + "pooledHostPool": "[if(equals(split(parameters('hostPoolType'), ' ')[0], 'Pooled'), true(), false())]", + "resourceGroups": "[union(variables('resourceGroupsCommon'), variables('resourceGroupsNetworking'), variables('resourceGroupsStorage'))]", + "resourceGroupsCommon": [ + "[parameters('resourceGroupControlPlane')]", + "[parameters('resourceGroupFeedWorkspace')]", + "[parameters('resourceGroupHosts')]", + "[parameters('resourceGroupManagement')]" + ], + "resourceGroupsNetworking": "[if(equals(length(parameters('deploymentLocations')), 2), parameters('resourceGroupsNetwork'), createArray(parameters('resourceGroupsNetwork')[0]))]", + "resourceGroupsStorage": "[if(variables('fslogix'), createArray(parameters('resourceGroupStorage')), createArray())]", + "roleDefinitions": { + "DesktopVirtualizationPowerOnContributor": "489581de-a3bd-480d-9518-53dea7416b33", + "DesktopVirtualizationUser": "1d18fff3-a72a-46b5-b4a9-0b38a3cd7e63", + "Reader": "acdd72a7-3385-48ef-bd42-f606fba81ae7", + "VirtualMachineUserLogin": "fb879df8-f326-4884-b1cf-06f3ad86be52" + }, + "securityPrincipalsCount": "[length(parameters('securityPrincipals'))]", + "smbServerLocation": "[parameters('locations')[parameters('locationVirtualMachines')].abbreviation]", + "storageSku": "[if(equals(parameters('fslogixStorageService'), 'None'), 'None', split(parameters('fslogixStorageService'), ' ')[1])]", + "storageService": "[split(parameters('fslogixStorageService'), ' ')[0]]", + "storageSuffix": "[environment().suffixes.storage]", + "timeDifference": "[parameters('locations')[parameters('locationVirtualMachines')].timeDifference]", + "timeZone": "[parameters('locations')[parameters('locationVirtualMachines')].timeZone]", + "vmTemplate": "[format('{{\"domain\":\"{0}\",\"galleryImageOffer\":\"{1}\",\"galleryImagePublisher\":\"{2}\",\"galleryImageSKU\":\"{3}\",\"imageType\":\"Gallery\",\"imageUri\":null,\"customImageId\":null,\"namePrefix\":\"{4}\",\"osDiskType\":\"{5}\",\"useManagedDisks\":true,\"VirtualMachineSize\":{{\"id\":\"{6}\",\"cores\":null,\"ram\":null}},\"galleryItemId\":\"{7}.{8}{9}\"}}', parameters('domainName'), parameters('imageOffer'), parameters('imagePublisher'), parameters('imageSku'), parameters('virtualMachineNamePrefix'), parameters('diskSku'), parameters('virtualMachineSize'), parameters('imagePublisher'), parameters('imageOffer'), parameters('imageSku'))]" + }, + "resources": [], + "outputs": { + "availabilitySetsCount": { + "type": "int", + "value": "[variables('availabilitySetsCount')]" + }, + "beginAvSetRange": { + "type": "int", + "value": "[variables('beginAvSetRange')]" + }, + "divisionRemainderValue": { + "type": "int", + "value": "[variables('divisionRemainderValue')]" + }, + "fileShares": { + "type": "array", + "value": "[variables('fileShares')]" + }, + "fslogix": { + "type": "bool", + "value": "[variables('fslogix')]" + }, + "maxResourcesPerTemplateDeployment": { + "type": "int", + "value": "[variables('maxResourcesPerTemplateDeployment')]" + }, + "netbios": { + "type": "string", + "value": "[variables('netbios')]" + }, + "pooledHostPool": { + "type": "bool", + "value": "[variables('pooledHostPool')]" + }, + "resourceGroups": { + "type": "array", + "value": "[variables('resourceGroups')]" + }, + "roleDefinitions": { + "type": "object", + "value": "[variables('roleDefinitions')]" + }, + "sessionHostBatchCount": { + "type": "int", + "value": "[variables('sessionHostBatchCount')]" + }, + "securityPrincipalsCount": { + "type": "int", + "value": "[variables('securityPrincipalsCount')]" + }, + "smbServerLocation": { + "type": "string", + "value": "[variables('smbServerLocation')]" + }, + "storageSku": { + "type": "string", + "value": "[variables('storageSku')]" + }, + "storageService": { + "type": "string", + "value": "[variables('storageService')]" + }, + "storageSuffix": { + "type": "string", + "value": "[variables('storageSuffix')]" + }, + "timeDifference": { + "type": "string", + "value": "[variables('timeDifference')]" + }, + "timeZone": { + "type": "string", + "value": "[variables('timeZone')]" + }, + "vmTemplate": { + "type": "string", + "value": "[variables('vmTemplate')]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp')))]" + ] + }, + { + "copy": { + "name": "rgs", + "count": "[length(range(0, variables('resourceGroupsCount')))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('ResourceGroup_{0}_{1}', range(0, variables('resourceGroupsCount'))[copyIndex()], parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": "[if(or(contains(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroups.value[range(0, variables('resourceGroupsCount'))[copyIndex()]], 'controlPlane'), contains(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroups.value[range(0, variables('resourceGroupsCount'))[copyIndex()]], 'feedWorkspace')), createObject('value', parameters('locationControlPlane')), createObject('value', parameters('locationVirtualMachines')))]", + "resourceGroupName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroups.value[range(0, variables('resourceGroupsCount'))[copyIndex()]]]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "938236069915523781" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "resourceGroupName": { + "type": "string" + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2020-10-01", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.Resources/resourceGroups'), parameters('tags')['Microsoft.Resources/resourceGroups'], createObject())]" + } + ] + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Network_ControlPlane_{0}', parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "azureNetAppFilesSubnetAddressPrefix": "[if(and(not(empty(parameters('azureNetAppFilesSubnetAddressPrefix'))), equals(length(variables('deploymentLocations')), 1)), createObject('value', parameters('azureNetAppFilesSubnetAddressPrefix')), createObject('value', ''))]", + "disableBgpRoutePropagation": { + "value": "[parameters('disableBgpRoutePropagation')]" + }, + "hubAzureFirewallResourceId": { + "value": "[parameters('hubAzureFirewallResourceId')]" + }, + "hubVirtualNetworkResourceId": { + "value": "[parameters('hubVirtualNetworkResourceId')]" + }, + "index": { + "value": 0 + }, + "location": { + "value": "[variables('deploymentLocations')[0]]" + }, + "networkSecurityGroupName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.networkSecurityGroupNames.value[0]]" + }, + "resourceGroupNetwork": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupsNetwork.value[0]]" + }, + "routeTableName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.routeTables.value[0]]" + }, + "subnetAddressPrefixes": { + "value": "[parameters('subnetAddressPrefixes')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "virtualNetworkAddressPrefixes": { + "value": "[parameters('virtualNetworkAddressPrefixes')]" + }, + "virtualNetworkName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.virtulNetworkNames.value[0]]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "17640310395260892141" + } + }, + "parameters": { + "azureNetAppFilesSubnetAddressPrefix": { + "type": "string" + }, + "disableBgpRoutePropagation": { + "type": "bool" + }, + "hubAzureFirewallResourceId": { + "type": "string" + }, + "hubVirtualNetworkResourceId": { + "type": "string" + }, + "index": { + "type": "int" + }, + "location": { + "type": "string" + }, + "networkSecurityGroupName": { + "type": "string" + }, + "subnetAddressPrefixes": { + "type": "array" + }, + "resourceGroupNetwork": { + "type": "string" + }, + "routeTableName": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "virtualNetworkAddressPrefixes": { + "type": "array" + }, + "virtualNetworkName": { + "type": "string" + } + }, + "variables": { + "hubSubscriptionId": "[split(parameters('hubVirtualNetworkResourceId'), '/')[2]]", + "hubVirtualNetworkName": "[split(parameters('hubVirtualNetworkResourceId'), '/')[8]]", + "hubVirtualNetworkResourceGroupName": "[split(parameters('hubVirtualNetworkResourceId'), '/')[4]]", + "networkSecurityGroupSecurityRules": [], + "spokeResourceGroup": "[parameters('resourceGroupNetwork')]", + "spokeSubscriptionId": "[subscription().subscriptionId]", + "subnets": "[union(variables('subnetWorkload'), variables('subnetAnf'))]", + "subnetAnf": "[if(empty(parameters('azureNetAppFilesSubnetAddressPrefix')), createArray(), createArray(createObject('name', 'AzureNetAppFiles', 'addressPrefix', parameters('azureNetAppFilesSubnetAddressPrefix'), 'delegations', createArray(createObject('name', 'Microsoft.Netapp.volumes', 'id', format('{0}/delegations/Microsoft.Netapp.volumes', resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), 'AzureNetAppFiles')), 'properties', createObject('serviceName', 'Microsoft.Netapp/volumes'), 'type', 'Microsoft.Network/virtualNetworks/subnets/delegations')), 'privateEndpointNetworkPolicies', 'Disabled', 'privateLinkServiceNetworkPolicies', 'Disabled', 'networkSecurityGroupName', parameters('networkSecurityGroupName'))))]", + "subnetWorkload": [ + { + "name": "AzureVirtualDesktop", + "addressPrefix": "[parameters('subnetAddressPrefixes')[parameters('index')]]", + "delegations": [], + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Disabled", + "networkSecurityGroupName": "[parameters('networkSecurityGroupName')]" + } + ] + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('UserDefinedRoute_{0}_{1}', parameters('index'), parameters('timestamp'))]", + "subscriptionId": "[variables('spokeSubscriptionId')]", + "resourceGroup": "[variables('spokeResourceGroup')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "azureFirewallIpAddress": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(parameters('hubAzureFirewallResourceId'), '/')[2], split(parameters('hubAzureFirewallResourceId'), '/')[4]), 'Microsoft.Network/azureFirewalls', split(parameters('hubAzureFirewallResourceId'), '/')[8]), '2023-05-01').ipConfigurations[0].properties.privateIPAddress]" + }, + "disableBgpRoutePropagation": { + "value": "[parameters('disableBgpRoutePropagation')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "udrName": { + "value": "[parameters('routeTableName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12548410719006982761" + } + }, + "parameters": { + "azureFirewallIpAddress": { + "type": "string" + }, + "disableBgpRoutePropagation": { + "type": "bool" + }, + "location": { + "type": "string" + }, + "udrName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/routeTables", + "apiVersion": "2021-05-01", + "name": "[parameters('udrName')]", + "location": "[parameters('location')]", + "properties": { + "disableBgpRoutePropagation": "[parameters('disableBgpRoutePropagation')]", + "routes": [ + { + "name": "default", + "properties": { + "addressPrefix": "0.0.0.0/0", + "hasBgpOverride": false, + "nextHopIpAddress": "[parameters('azureFirewallIpAddress')]", + "nextHopType": "VirtualAppliance" + } + } + ] + } + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('udrName')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.Network/routeTables', parameters('udrName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('NetworkSecurityGroup_{0}_{1}', parameters('index'), parameters('timestamp'))]", + "subscriptionId": "[variables('spokeSubscriptionId')]", + "resourceGroup": "[variables('spokeResourceGroup')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "networkSecurityGroupName": { + "value": "[parameters('networkSecurityGroupName')]" + }, + "networkSecurityGroupSecurityRules": { + "value": "[variables('networkSecurityGroupSecurityRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "9612737696029571662" + } + }, + "parameters": { + "networkSecurityGroupSecurityRules": { + "type": "array" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "networkSecurityGroupName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2020-11-01", + "name": "[parameters('networkSecurityGroupName')]", + "location": "[parameters('location')]", + "properties": { + "copy": [ + { + "name": "securityRules", + "count": "[length(parameters('networkSecurityGroupSecurityRules'))]", + "input": { + "name": "[parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].name]", + "properties": { + "access": "[parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.access]", + "destinationAddressPrefix": "[if(equals(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationAddressPrefix, ''), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationAddressPrefix)]", + "destinationAddressPrefixes": "[if(equals(length(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationAddressPrefixes), 0), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationAddressPrefixes)]", + "destinationPortRanges": "[if(equals(length(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationPortRanges), 0), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationPortRanges)]", + "destinationPortRange": "[if(equals(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationPortRange, ''), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationPortRange)]", + "direction": "[parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.direction]", + "priority": "[int(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.priority)]", + "protocol": "[parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.protocol]", + "sourceAddressPrefix": "[if(equals(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.sourceAddressPrefix, ''), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.sourceAddressPrefix)]", + "sourcePortRanges": "[if(equals(length(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.sourcePortRanges), 0), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.sourcePortRanges)]", + "sourcePortRange": "[parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.sourcePortRange]" + } + } + } + ] + } + } + ] + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp'))]", + "subscriptionId": "[variables('spokeSubscriptionId')]", + "resourceGroup": "[variables('spokeResourceGroup')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "dnsServers": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('hubSubscriptionId'), variables('hubVirtualNetworkResourceGroupName')), 'Microsoft.Network/virtualNetworks', variables('hubVirtualNetworkName')), '2023-05-01').dhcpOptions.dnsServers]" + }, + "location": { + "value": "[parameters('location')]" + }, + "subnets": { + "value": "[variables('subnets')]" + }, + "udrName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('UserDefinedRoute_{0}_{1}', parameters('index'), parameters('timestamp'))), '2022-09-01').outputs.name.value]" + }, + "virtualNetworkName": { + "value": "[parameters('virtualNetworkName')]" + }, + "vNetAddressPrefixes": { + "value": [ + "[parameters('virtualNetworkAddressPrefixes')[parameters('index')]]" + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "13136571387938513749" + } + }, + "parameters": { + "dnsServers": { + "type": "array" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "virtualNetworkName": { + "type": "string" + }, + "subnets": { + "type": "array" + }, + "udrName": { + "type": "string" + }, + "vNetAddressPrefixes": { + "type": "array" + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2020-11-01", + "name": "[parameters('virtualNetworkName')]", + "location": "[parameters('location')]", + "properties": { + "copy": [ + { + "name": "subnets", + "count": "[length(parameters('subnets'))]", + "input": { + "name": "[parameters('subnets')[copyIndex('subnets')].name]", + "properties": { + "addressPrefix": "[parameters('subnets')[copyIndex('subnets')].addressPrefix]", + "delegations": "[parameters('subnets')[copyIndex('subnets')].delegations]", + "networkSecurityGroup": "[if(empty(parameters('subnets')[copyIndex('subnets')].networkSecurityGroupName), null(), json(format('{{\"id\": \"{0}\"}}', resourceId('Microsoft.Network/networkSecurityGroups', parameters('subnets')[copyIndex('subnets')].networkSecurityGroupName))))]", + "privateEndpointNetworkPolicies": "[parameters('subnets')[copyIndex('subnets')].privateEndpointNetworkPolicies]", + "privateLinkServiceNetworkPolicies": "[parameters('subnets')[copyIndex('subnets')].privateLinkServiceNetworkPolicies]", + "routeTable": { + "id": "[resourceId('Microsoft.Network/routeTables', parameters('udrName'))]" + } + } + } + } + ], + "addressSpace": { + "addressPrefixes": "[parameters('vNetAddressPrefixes')]" + }, + "dhcpOptions": { + "dnsServers": "[parameters('dnsServers')]" + } + } + } + ], + "outputs": { + "virtualNetworkName": { + "type": "string", + "value": "[parameters('virtualNetworkName')]" + }, + "virtualNetworkResourceId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" + }, + "subnetResourceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName')), '2020-11-01').subnets[0].id]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('UserDefinedRoute_{0}_{1}', parameters('index'), parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('VirtualNetworkPeer_Hub_{0}_{1}', parameters('index'), parameters('timestamp'))]", + "subscriptionId": "[variables('spokeSubscriptionId')]", + "resourceGroup": "[variables('spokeResourceGroup')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "existingLocalVirtualNetworkName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp'))), '2022-09-01').outputs.virtualNetworkName.value]" + }, + "existingRemoteVirtualNetworkName": { + "value": "[variables('hubVirtualNetworkName')]" + }, + "existingRemoteVirtualNetworkResourceGroupName": { + "value": "[variables('hubVirtualNetworkResourceGroupName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "9008903421294659384" + } + }, + "parameters": { + "existingLocalVirtualNetworkName": { + "type": "string" + }, + "existingRemoteVirtualNetworkName": { + "type": "string" + }, + "existingRemoteVirtualNetworkResourceGroupName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks/virtualNetworkPeerings", + "apiVersion": "2021-02-01", + "name": "[format('{0}/{1}', parameters('existingLocalVirtualNetworkName'), parameters('existingRemoteVirtualNetworkName'))]", + "properties": { + "allowVirtualNetworkAccess": true, + "allowForwardedTraffic": true, + "allowGatewayTransit": false, + "useRemoteGateways": false, + "remoteVirtualNetwork": { + "id": "[resourceId(parameters('existingRemoteVirtualNetworkResourceGroupName'), 'Microsoft.Network/virtualNetworks', parameters('existingRemoteVirtualNetworkName'))]" + } + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('VirtualNetworkPeer_Spoke_{0}_{1}', parameters('index'), parameters('timestamp'))]", + "subscriptionId": "[variables('hubSubscriptionId')]", + "resourceGroup": "[variables('hubVirtualNetworkResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "existingLocalVirtualNetworkName": { + "value": "[variables('hubVirtualNetworkName')]" + }, + "existingRemoteVirtualNetworkName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp'))), '2022-09-01').outputs.virtualNetworkName.value]" + }, + "existingRemoteVirtualNetworkResourceGroupName": { + "value": "[variables('spokeResourceGroup')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "9008903421294659384" + } + }, + "parameters": { + "existingLocalVirtualNetworkName": { + "type": "string" + }, + "existingRemoteVirtualNetworkName": { + "type": "string" + }, + "existingRemoteVirtualNetworkResourceGroupName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks/virtualNetworkPeerings", + "apiVersion": "2021-02-01", + "name": "[format('{0}/{1}', parameters('existingLocalVirtualNetworkName'), parameters('existingRemoteVirtualNetworkName'))]", + "properties": { + "allowVirtualNetworkAccess": true, + "allowForwardedTraffic": true, + "allowGatewayTransit": false, + "useRemoteGateways": false, + "remoteVirtualNetwork": { + "id": "[resourceId(parameters('existingRemoteVirtualNetworkResourceGroupName'), 'Microsoft.Network/virtualNetworks', parameters('existingRemoteVirtualNetworkName'))]" + } + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetworkPeer_Hub_{0}_{1}', parameters('index'), parameters('timestamp')))]" + ] + } + ], + "outputs": { + "subnetResourceId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp')))]", + "rgs" + ] + }, + { + "condition": "[equals(length(variables('deploymentLocations')), 2)]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Network_Hosts_{0}', parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "azureNetAppFilesSubnetAddressPrefix": "[if(and(not(empty(parameters('azureNetAppFilesSubnetAddressPrefix'))), equals(length(variables('deploymentLocations')), 2)), createObject('value', parameters('azureNetAppFilesSubnetAddressPrefix')), createObject('value', ''))]", + "disableBgpRoutePropagation": { + "value": "[parameters('disableBgpRoutePropagation')]" + }, + "hubAzureFirewallResourceId": { + "value": "[parameters('hubAzureFirewallResourceId')]" + }, + "hubVirtualNetworkResourceId": { + "value": "[parameters('hubVirtualNetworkResourceId')]" + }, + "index": { + "value": 1 + }, + "location": { + "value": "[variables('deploymentLocations')[1]]" + }, + "networkSecurityGroupName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.networkSecurityGroupNames.value[1]]" + }, + "resourceGroupNetwork": "[if(equals(length(variables('deploymentLocations')), 1), createObject('value', reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupsNetwork.value[0]), createObject('value', reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupsNetwork.value[1]))]", + "routeTableName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.routeTables.value[1]]" + }, + "subnetAddressPrefixes": { + "value": "[parameters('subnetAddressPrefixes')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "virtualNetworkAddressPrefixes": { + "value": "[parameters('virtualNetworkAddressPrefixes')]" + }, + "virtualNetworkName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.virtulNetworkNames.value[1]]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "17640310395260892141" + } + }, + "parameters": { + "azureNetAppFilesSubnetAddressPrefix": { + "type": "string" + }, + "disableBgpRoutePropagation": { + "type": "bool" + }, + "hubAzureFirewallResourceId": { + "type": "string" + }, + "hubVirtualNetworkResourceId": { + "type": "string" + }, + "index": { + "type": "int" + }, + "location": { + "type": "string" + }, + "networkSecurityGroupName": { + "type": "string" + }, + "subnetAddressPrefixes": { + "type": "array" + }, + "resourceGroupNetwork": { + "type": "string" + }, + "routeTableName": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "virtualNetworkAddressPrefixes": { + "type": "array" + }, + "virtualNetworkName": { + "type": "string" + } + }, + "variables": { + "hubSubscriptionId": "[split(parameters('hubVirtualNetworkResourceId'), '/')[2]]", + "hubVirtualNetworkName": "[split(parameters('hubVirtualNetworkResourceId'), '/')[8]]", + "hubVirtualNetworkResourceGroupName": "[split(parameters('hubVirtualNetworkResourceId'), '/')[4]]", + "networkSecurityGroupSecurityRules": [], + "spokeResourceGroup": "[parameters('resourceGroupNetwork')]", + "spokeSubscriptionId": "[subscription().subscriptionId]", + "subnets": "[union(variables('subnetWorkload'), variables('subnetAnf'))]", + "subnetAnf": "[if(empty(parameters('azureNetAppFilesSubnetAddressPrefix')), createArray(), createArray(createObject('name', 'AzureNetAppFiles', 'addressPrefix', parameters('azureNetAppFilesSubnetAddressPrefix'), 'delegations', createArray(createObject('name', 'Microsoft.Netapp.volumes', 'id', format('{0}/delegations/Microsoft.Netapp.volumes', resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), 'AzureNetAppFiles')), 'properties', createObject('serviceName', 'Microsoft.Netapp/volumes'), 'type', 'Microsoft.Network/virtualNetworks/subnets/delegations')), 'privateEndpointNetworkPolicies', 'Disabled', 'privateLinkServiceNetworkPolicies', 'Disabled', 'networkSecurityGroupName', parameters('networkSecurityGroupName'))))]", + "subnetWorkload": [ + { + "name": "AzureVirtualDesktop", + "addressPrefix": "[parameters('subnetAddressPrefixes')[parameters('index')]]", + "delegations": [], + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Disabled", + "networkSecurityGroupName": "[parameters('networkSecurityGroupName')]" + } + ] + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('UserDefinedRoute_{0}_{1}', parameters('index'), parameters('timestamp'))]", + "subscriptionId": "[variables('spokeSubscriptionId')]", + "resourceGroup": "[variables('spokeResourceGroup')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "azureFirewallIpAddress": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(parameters('hubAzureFirewallResourceId'), '/')[2], split(parameters('hubAzureFirewallResourceId'), '/')[4]), 'Microsoft.Network/azureFirewalls', split(parameters('hubAzureFirewallResourceId'), '/')[8]), '2023-05-01').ipConfigurations[0].properties.privateIPAddress]" + }, + "disableBgpRoutePropagation": { + "value": "[parameters('disableBgpRoutePropagation')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "udrName": { + "value": "[parameters('routeTableName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12548410719006982761" + } + }, + "parameters": { + "azureFirewallIpAddress": { + "type": "string" + }, + "disableBgpRoutePropagation": { + "type": "bool" + }, + "location": { + "type": "string" + }, + "udrName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/routeTables", + "apiVersion": "2021-05-01", + "name": "[parameters('udrName')]", + "location": "[parameters('location')]", + "properties": { + "disableBgpRoutePropagation": "[parameters('disableBgpRoutePropagation')]", + "routes": [ + { + "name": "default", + "properties": { + "addressPrefix": "0.0.0.0/0", + "hasBgpOverride": false, + "nextHopIpAddress": "[parameters('azureFirewallIpAddress')]", + "nextHopType": "VirtualAppliance" + } + } + ] + } + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('udrName')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.Network/routeTables', parameters('udrName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('NetworkSecurityGroup_{0}_{1}', parameters('index'), parameters('timestamp'))]", + "subscriptionId": "[variables('spokeSubscriptionId')]", + "resourceGroup": "[variables('spokeResourceGroup')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "networkSecurityGroupName": { + "value": "[parameters('networkSecurityGroupName')]" + }, + "networkSecurityGroupSecurityRules": { + "value": "[variables('networkSecurityGroupSecurityRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "9612737696029571662" + } + }, + "parameters": { + "networkSecurityGroupSecurityRules": { + "type": "array" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "networkSecurityGroupName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2020-11-01", + "name": "[parameters('networkSecurityGroupName')]", + "location": "[parameters('location')]", + "properties": { + "copy": [ + { + "name": "securityRules", + "count": "[length(parameters('networkSecurityGroupSecurityRules'))]", + "input": { + "name": "[parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].name]", + "properties": { + "access": "[parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.access]", + "destinationAddressPrefix": "[if(equals(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationAddressPrefix, ''), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationAddressPrefix)]", + "destinationAddressPrefixes": "[if(equals(length(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationAddressPrefixes), 0), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationAddressPrefixes)]", + "destinationPortRanges": "[if(equals(length(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationPortRanges), 0), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationPortRanges)]", + "destinationPortRange": "[if(equals(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationPortRange, ''), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.destinationPortRange)]", + "direction": "[parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.direction]", + "priority": "[int(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.priority)]", + "protocol": "[parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.protocol]", + "sourceAddressPrefix": "[if(equals(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.sourceAddressPrefix, ''), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.sourceAddressPrefix)]", + "sourcePortRanges": "[if(equals(length(parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.sourcePortRanges), 0), null(), parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.sourcePortRanges)]", + "sourcePortRange": "[parameters('networkSecurityGroupSecurityRules')[copyIndex('securityRules')].properties.sourcePortRange]" + } + } + } + ] + } + } + ] + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp'))]", + "subscriptionId": "[variables('spokeSubscriptionId')]", + "resourceGroup": "[variables('spokeResourceGroup')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "dnsServers": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('hubSubscriptionId'), variables('hubVirtualNetworkResourceGroupName')), 'Microsoft.Network/virtualNetworks', variables('hubVirtualNetworkName')), '2023-05-01').dhcpOptions.dnsServers]" + }, + "location": { + "value": "[parameters('location')]" + }, + "subnets": { + "value": "[variables('subnets')]" + }, + "udrName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('UserDefinedRoute_{0}_{1}', parameters('index'), parameters('timestamp'))), '2022-09-01').outputs.name.value]" + }, + "virtualNetworkName": { + "value": "[parameters('virtualNetworkName')]" + }, + "vNetAddressPrefixes": { + "value": [ + "[parameters('virtualNetworkAddressPrefixes')[parameters('index')]]" + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "13136571387938513749" + } + }, + "parameters": { + "dnsServers": { + "type": "array" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "virtualNetworkName": { + "type": "string" + }, + "subnets": { + "type": "array" + }, + "udrName": { + "type": "string" + }, + "vNetAddressPrefixes": { + "type": "array" + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2020-11-01", + "name": "[parameters('virtualNetworkName')]", + "location": "[parameters('location')]", + "properties": { + "copy": [ + { + "name": "subnets", + "count": "[length(parameters('subnets'))]", + "input": { + "name": "[parameters('subnets')[copyIndex('subnets')].name]", + "properties": { + "addressPrefix": "[parameters('subnets')[copyIndex('subnets')].addressPrefix]", + "delegations": "[parameters('subnets')[copyIndex('subnets')].delegations]", + "networkSecurityGroup": "[if(empty(parameters('subnets')[copyIndex('subnets')].networkSecurityGroupName), null(), json(format('{{\"id\": \"{0}\"}}', resourceId('Microsoft.Network/networkSecurityGroups', parameters('subnets')[copyIndex('subnets')].networkSecurityGroupName))))]", + "privateEndpointNetworkPolicies": "[parameters('subnets')[copyIndex('subnets')].privateEndpointNetworkPolicies]", + "privateLinkServiceNetworkPolicies": "[parameters('subnets')[copyIndex('subnets')].privateLinkServiceNetworkPolicies]", + "routeTable": { + "id": "[resourceId('Microsoft.Network/routeTables', parameters('udrName'))]" + } + } + } + } + ], + "addressSpace": { + "addressPrefixes": "[parameters('vNetAddressPrefixes')]" + }, + "dhcpOptions": { + "dnsServers": "[parameters('dnsServers')]" + } + } + } + ], + "outputs": { + "virtualNetworkName": { + "type": "string", + "value": "[parameters('virtualNetworkName')]" + }, + "virtualNetworkResourceId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" + }, + "subnetResourceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName')), '2020-11-01').subnets[0].id]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('UserDefinedRoute_{0}_{1}', parameters('index'), parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('VirtualNetworkPeer_Hub_{0}_{1}', parameters('index'), parameters('timestamp'))]", + "subscriptionId": "[variables('spokeSubscriptionId')]", + "resourceGroup": "[variables('spokeResourceGroup')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "existingLocalVirtualNetworkName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp'))), '2022-09-01').outputs.virtualNetworkName.value]" + }, + "existingRemoteVirtualNetworkName": { + "value": "[variables('hubVirtualNetworkName')]" + }, + "existingRemoteVirtualNetworkResourceGroupName": { + "value": "[variables('hubVirtualNetworkResourceGroupName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "9008903421294659384" + } + }, + "parameters": { + "existingLocalVirtualNetworkName": { + "type": "string" + }, + "existingRemoteVirtualNetworkName": { + "type": "string" + }, + "existingRemoteVirtualNetworkResourceGroupName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks/virtualNetworkPeerings", + "apiVersion": "2021-02-01", + "name": "[format('{0}/{1}', parameters('existingLocalVirtualNetworkName'), parameters('existingRemoteVirtualNetworkName'))]", + "properties": { + "allowVirtualNetworkAccess": true, + "allowForwardedTraffic": true, + "allowGatewayTransit": false, + "useRemoteGateways": false, + "remoteVirtualNetwork": { + "id": "[resourceId(parameters('existingRemoteVirtualNetworkResourceGroupName'), 'Microsoft.Network/virtualNetworks', parameters('existingRemoteVirtualNetworkName'))]" + } + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('VirtualNetworkPeer_Spoke_{0}_{1}', parameters('index'), parameters('timestamp'))]", + "subscriptionId": "[variables('hubSubscriptionId')]", + "resourceGroup": "[variables('hubVirtualNetworkResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "existingLocalVirtualNetworkName": { + "value": "[variables('hubVirtualNetworkName')]" + }, + "existingRemoteVirtualNetworkName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp'))), '2022-09-01').outputs.virtualNetworkName.value]" + }, + "existingRemoteVirtualNetworkResourceGroupName": { + "value": "[variables('spokeResourceGroup')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "9008903421294659384" + } + }, + "parameters": { + "existingLocalVirtualNetworkName": { + "type": "string" + }, + "existingRemoteVirtualNetworkName": { + "type": "string" + }, + "existingRemoteVirtualNetworkResourceGroupName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks/virtualNetworkPeerings", + "apiVersion": "2021-02-01", + "name": "[format('{0}/{1}', parameters('existingLocalVirtualNetworkName'), parameters('existingRemoteVirtualNetworkName'))]", + "properties": { + "allowVirtualNetworkAccess": true, + "allowForwardedTraffic": true, + "allowGatewayTransit": false, + "useRemoteGateways": false, + "remoteVirtualNetwork": { + "id": "[resourceId(parameters('existingRemoteVirtualNetworkResourceGroupName'), 'Microsoft.Network/virtualNetworks', parameters('existingRemoteVirtualNetworkName'))]" + } + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetworkPeer_Hub_{0}_{1}', parameters('index'), parameters('timestamp')))]" + ] + } + ], + "outputs": { + "subnetResourceId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('spokeSubscriptionId'), variables('spokeResourceGroup')), 'Microsoft.Resources/deployments', format('VirtualNetwork_{0}_{1}', parameters('index'), parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp')))]", + "rgs" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Management_{0}', parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "activeDirectorySolution": { + "value": "[parameters('activeDirectorySolution')]" + }, + "artifactsStorageAccountResourceId": { + "value": "[parameters('artifactsStorageAccountResourceId')]" + }, + "artifactsUri": { + "value": "[variables('artifactsUri')]" + }, + "automationAccountName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.automationAccountName.value]" + }, + "automationAccountPrivateDnsZoneResourceId": { + "value": "[format('{0}{1}', variables('privateDnsZoneResourceIdPrefix'), reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.azureAutomationPrivateDnsZoneName.value)]" + }, + "availability": { + "value": "[parameters('availability')]" + }, + "avdObjectId": { + "value": "[parameters('avdObjectId')]" + }, + "azureBlobsPrivateDnsZoneResourceId": { + "value": "[format('{0}{1}', variables('privateDnsZoneResourceIdPrefix'), reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.blobPrivateDnsZoneName.value)]" + }, + "azurePowerShellModuleMsiName": { + "value": "[parameters('azurePowerShellModuleMsiName')]" + }, + "azureQueueStoragePrivateDnsZoneResourceId": { + "value": "[format('{0}{1}', variables('privateDnsZoneResourceIdPrefix'), reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.queuePrivateDnsZoneName.value)]" + }, + "dataCollectionRuleName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.dataCollectionRuleName.value]" + }, + "diskEncryptionSetName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.diskEncryptionSetName.value]" + }, + "diskNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.diskNamePrefix.value]" + }, + "diskSku": { + "value": "[parameters('diskSku')]" + }, + "domainJoinPassword": { + "value": "[parameters('domainJoinPassword')]" + }, + "domainJoinUserPrincipalName": { + "value": "[parameters('domainJoinUserPrincipalName')]" + }, + "domainName": { + "value": "[parameters('domainName')]" + }, + "enableMonitoring": { + "value": "[parameters('monitoring')]" + }, + "environmentShortName": { + "value": "[parameters('environmentShortName')]" + }, + "fslogix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.fslogix.value]" + }, + "fslogixStorageService": { + "value": "[parameters('fslogixStorageService')]" + }, + "hostPoolName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.hostPoolName.value]" + }, + "hostPoolType": { + "value": "[parameters('hostPoolType')]" + }, + "imageDefinitionResourceId": { + "value": "[parameters('imageDefinitionResourceId')]" + }, + "keyVaultAbbreviation": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceAbbreviations.value.keyVaults]" + }, + "keyVaultName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.keyVaultName.value]" + }, + "keyVaultPrivateDnsZoneResourceId": { + "value": "[format('{0}{1}', variables('privateDnsZoneResourceIdPrefix'), reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.keyVaultPrivateDnsZoneName.value)]" + }, + "locationVirtualMachines": { + "value": "[parameters('locationVirtualMachines')]" + }, + "logAnalyticsWorkspaceName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.logAnalyticsWorkspaceName.value]" + }, + "logAnalyticsWorkspaceRetention": { + "value": "[parameters('logAnalyticsWorkspaceRetention')]" + }, + "logAnalyticsWorkspaceSku": { + "value": "[parameters('logAnalyticsWorkspaceSku')]" + }, + "networkInterfaceNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.networkInterfaceNamePrefix.value]" + }, + "organizationalUnitPath": { + "value": "[parameters('organizationalUnitPath')]" + }, + "recoveryServices": { + "value": "[parameters('recoveryServices')]" + }, + "recoveryServicesPrivateDnsZoneResourceId": { + "value": "[format('{0}{1}', variables('privateDnsZoneResourceIdPrefix'), reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.backupPrivateDnsZoneName.value)]" + }, + "recoveryServicesVaultName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.recoveryServicesVaultName.value]" + }, + "resourceGroupControlPlane": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupControlPlane.value]" + }, + "resourceGroupFeedWorkspace": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupFeedWorkspace.value]" + }, + "resourceGroupHosts": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupHosts.value]" + }, + "resourceGroupManagement": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupManagement.value]" + }, + "resourceGroupStorage": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupStorage.value]" + }, + "roleDefinitions": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.roleDefinitions.value]" + }, + "scalingTool": { + "value": "[parameters('scalingTool')]" + }, + "securityLogAnalyticsWorkspaceResourceId": { + "value": "[parameters('securityLogAnalyticsWorkspaceResourceId')]" + }, + "sessionHostCount": { + "value": "[parameters('sessionHostCount')]" + }, + "storageService": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.storageService.value]" + }, + "subnetResourceId": "[if(equals(length(variables('deploymentLocations')), 1), createObject('value', reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value), createObject('value', reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_Hosts_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value))]", + "tags": { + "value": "[parameters('tags')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "timeZone": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.timeZone.value]" + }, + "userAssignedIdentityNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.userAssignedIdentityNamePrefix.value]" + }, + "virtualMachineMonitoringAgent": { + "value": "[parameters('virtualMachineMonitoringAgent')]" + }, + "virtualMachineNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.virtualMachineNamePrefix.value]" + }, + "virtualMachinePassword": { + "value": "[parameters('virtualMachinePassword')]" + }, + "virtualMachineSize": { + "value": "[parameters('virtualMachineSize')]" + }, + "virtualMachineUsername": { + "value": "[parameters('virtualMachineUsername')]" + }, + "workspaceNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.workspaceFeedNamePrefix.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "18151695860303397598" + } + }, + "parameters": { + "activeDirectorySolution": { + "type": "string" + }, + "artifactsUri": { + "type": "string" + }, + "artifactsStorageAccountResourceId": { + "type": "string" + }, + "automationAccountName": { + "type": "string" + }, + "automationAccountPrivateDnsZoneResourceId": { + "type": "string" + }, + "availability": { + "type": "string" + }, + "avdObjectId": { + "type": "string" + }, + "azureBlobsPrivateDnsZoneResourceId": { + "type": "string" + }, + "azurePowerShellModuleMsiName": { + "type": "string" + }, + "azureQueueStoragePrivateDnsZoneResourceId": { + "type": "string" + }, + "dataCollectionRuleName": { + "type": "string" + }, + "diskNamePrefix": { + "type": "string" + }, + "diskEncryptionSetName": { + "type": "string" + }, + "diskSku": { + "type": "string" + }, + "domainJoinPassword": { + "type": "securestring" + }, + "domainJoinUserPrincipalName": { + "type": "string" + }, + "domainName": { + "type": "string" + }, + "enableMonitoring": { + "type": "bool" + }, + "environmentShortName": { + "type": "string" + }, + "fslogix": { + "type": "bool" + }, + "fslogixStorageService": { + "type": "string" + }, + "hostPoolName": { + "type": "string" + }, + "hostPoolType": { + "type": "string" + }, + "imageDefinitionResourceId": { + "type": "string" + }, + "keyVaultAbbreviation": { + "type": "string" + }, + "keyVaultName": { + "type": "string" + }, + "keyVaultPrivateDnsZoneResourceId": { + "type": "string" + }, + "locationVirtualMachines": { + "type": "string" + }, + "logAnalyticsWorkspaceName": { + "type": "string" + }, + "logAnalyticsWorkspaceRetention": { + "type": "int" + }, + "logAnalyticsWorkspaceSku": { + "type": "string" + }, + "networkInterfaceNamePrefix": { + "type": "string" + }, + "organizationalUnitPath": { + "type": "string" + }, + "recoveryServices": { + "type": "bool" + }, + "recoveryServicesPrivateDnsZoneResourceId": { + "type": "string" + }, + "recoveryServicesVaultName": { + "type": "string" + }, + "resourceGroupControlPlane": { + "type": "string" + }, + "resourceGroupFeedWorkspace": { + "type": "string" + }, + "resourceGroupHosts": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "resourceGroupStorage": { + "type": "string" + }, + "roleDefinitions": { + "type": "object" + }, + "scalingTool": { + "type": "bool" + }, + "securityLogAnalyticsWorkspaceResourceId": { + "type": "string" + }, + "sessionHostCount": { + "type": "int" + }, + "storageService": { + "type": "string" + }, + "subnetResourceId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "userAssignedIdentityNamePrefix": { + "type": "string" + }, + "virtualMachineMonitoringAgent": { + "type": "string" + }, + "virtualMachineNamePrefix": { + "type": "string" + }, + "virtualMachinePassword": { + "type": "securestring" + }, + "virtualMachineUsername": { + "type": "string" + }, + "virtualMachineSize": { + "type": "string" + }, + "workspaceNamePrefix": { + "type": "string" + } + }, + "variables": { + "CpuCountMax": "[if(contains(parameters('hostPoolType'), 'Pooled'), 32, 128)]", + "CpuCountMin": "[if(contains(parameters('hostPoolType'), 'Pooled'), 4, 2)]", + "roleAssignments": "[union(variables('roleAssignmentsCommon'), variables('roleAssignmentStorage'))]", + "roleAssignmentsCommon": [ + { + "roleDefinitionId": "f353d9bd-d4a6-484e-a77a-8050b599b867", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "subscription": "[subscription().subscriptionId]" + }, + { + "roleDefinitionId": "86240b0e-9422-4c43-887b-b61143f32ba8", + "resourceGroup": "[parameters('resourceGroupControlPlane')]", + "subscription": "[subscription().subscriptionId]" + }, + { + "roleDefinitionId": "2ad6aaab-ead9-4eaa-8ac5-da422f562408", + "resourceGroup": "[parameters('resourceGroupControlPlane')]", + "subscription": "[subscription().subscriptionId]" + }, + { + "roleDefinitionId": "a959dbd1-f747-45e3-8ba6-dd80f235f97c", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "subscription": "[subscription().subscriptionId]" + }, + { + "roleDefinitionId": "21efdde3-836f-432b-bf3d-3e8e734d4b2b", + "resourceGroup": "[parameters('resourceGroupFeedWorkspace')]", + "subscription": "[subscription().subscriptionId]" + }, + { + "roleDefinitionId": "2a2b9908-6ea1-4ae2-8e65-a410df84e7d1", + "resourceGroup": "[split(parameters('artifactsStorageAccountResourceId'), '/')[4]]", + "subscription": "[split(parameters('artifactsStorageAccountResourceId'), '/')[2]]" + } + ], + "roleAssignmentStorage": "[if(parameters('fslogix'), createArray(createObject('roleDefinitionId', '17d1049b-9a84-46fb-8f53-869881c3d3ab', 'resourceGroup', parameters('resourceGroupStorage'), 'subscription', subscription().subscriptionId)), createArray())]", + "VirtualNetworkName": "[split(parameters('subnetResourceId'), '/')[8]]", + "VirtualNetworkResourceGroupName": "[split(parameters('subnetResourceId'), '/')[4]]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(format('{0}-deployment', parameters('userAssignedIdentityNamePrefix')), parameters('roleDefinitions').Reader, subscription().id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitions').Reader)]", + "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp'))), '2022-09-01').outputs.principalId.value]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('avdObjectId'), parameters('roleDefinitions').DesktopVirtualizationPowerOnContributor, subscription().id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitions').DesktopVirtualizationPowerOnContributor)]", + "principalId": "[parameters('avdObjectId')]" + } + }, + { + "condition": "[and(contains(parameters('hostPoolType'), 'Pooled'), parameters('recoveryServices'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Policy_{0}', parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "resourceGroupName": { + "value": "[parameters('resourceGroupHosts')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "1743191604298359594" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "resourceGroupName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/policyDefinitions", + "apiVersion": "2021-06-01", + "name": "DiskNetworkAccess", + "properties": { + "description": "[format('Disable network access to managed disks in the {0} resource group', parameters('resourceGroupName'))]", + "displayName": "[format('Disable Disk Access ({0})', parameters('resourceGroupName'))]", + "mode": "All", + "parameters": {}, + "policyRule": { + "if": { + "field": "type", + "equals": "Microsoft.Compute/disks" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/60fc6e62-5479-42d4-8bf4-67625fcc2840" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "Microsoft.Compute/disks/networkAccessPolicy", + "value": "DenyAll" + }, + { + "operation": "addOrReplace", + "field": "Microsoft.Compute/disks/publicNetworkAccess", + "value": "Disabled" + } + ] + } + } + }, + "policyType": "Custom" + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "DiskNetworkAccess", + "resourceGroup": "[parameters('resourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "policyDefinitionId": { + "value": "[subscriptionResourceId('Microsoft.Authorization/policyDefinitions', 'DiskNetworkAccess')]" + }, + "policyDisplayName": { + "value": "[reference(subscriptionResourceId('Microsoft.Authorization/policyDefinitions', 'DiskNetworkAccess'), '2021-06-01').displayName]" + }, + "policyName": { + "value": "[reference(subscriptionResourceId('Microsoft.Authorization/policyDefinitions', 'DiskNetworkAccess'), '2021-06-01').displayName]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "5823942468104274443" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "policyDefinitionId": { + "type": "string" + }, + "policyDisplayName": { + "type": "string" + }, + "policyName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/policyAssignments", + "apiVersion": "2022-06-01", + "name": "[parameters('policyName')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "displayName": "[parameters('policyDisplayName')]", + "policyDefinitionId": "[parameters('policyDefinitionId')]" + } + } + ] + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Authorization/policyDefinitions', 'DiskNetworkAccess')]" + ] + } + ] + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('UserAssignedIdentity_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "name": { + "value": "[format('{0}-deployment', parameters('userAssignedIdentityNamePrefix'))]" + }, + "tags": "[if(contains(parameters('tags'), 'Microsoft.ManagedIdentity/userAssignedIdentities'), createObject('value', parameters('tags')['Microsoft.ManagedIdentity/userAssignedIdentities']), createObject('value', createObject()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "1968141880350840837" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2018-11-30", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "clientId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2018-11-30').clientId]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" + }, + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2018-11-30').principalId]" + } + } + } + } + }, + { + "copy": { + "name": "roleAssignments_deployment", + "count": "[length(range(0, length(variables('roleAssignments'))))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('RoleAssignment_{0}_{1}', range(0, length(variables('roleAssignments')))[copyIndex()], parameters('timestamp'))]", + "subscriptionId": "[variables('roleAssignments')[range(0, length(variables('roleAssignments')))[copyIndex()]].subscription]", + "resourceGroup": "[variables('roleAssignments')[range(0, length(variables('roleAssignments')))[copyIndex()]].resourceGroup]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "PrincipalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp'))), '2022-09-01').outputs.principalId.value]" + }, + "PrincipalType": { + "value": "ServicePrincipal" + }, + "RoleDefinitionId": { + "value": "[variables('roleAssignments')[range(0, length(variables('roleAssignments')))[copyIndex()]].roleDefinitionId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "3999918654980032531" + } + }, + "parameters": { + "PrincipalId": { + "type": "string" + }, + "PrincipalType": { + "type": "string" + }, + "RoleDefinitionId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('PrincipalId'), parameters('RoleDefinitionId'), resourceGroup().id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('RoleDefinitionId'))]", + "principalId": "[parameters('PrincipalId')]", + "principalType": "[parameters('PrincipalType')]" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Artifacts_{0}', parameters('timestamp'))]", + "subscriptionId": "[split(parameters('artifactsStorageAccountResourceId'), '/')[2]]", + "resourceGroup": "[split(parameters('artifactsStorageAccountResourceId'), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "resourceGroupManagement": { + "value": "[parameters('resourceGroupManagement')]" + }, + "storageAccountName": { + "value": "[split(parameters('artifactsStorageAccountResourceId'), '/')[8]]" + }, + "subscriptionId": { + "value": "[subscription().subscriptionId]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "userAssignedIdentityNamePrefix": { + "value": "[parameters('userAssignedIdentityNamePrefix')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "4306236320133689635" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "storageAccountName": { + "type": "string" + }, + "subscriptionId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "userAssignedIdentityNamePrefix": { + "type": "string" + } + }, + "variables": { + "name": "[format('{0}-artifacts', parameters('userAssignedIdentityNamePrefix'))]", + "roleDefinitionId": "2a2b9908-6ea1-4ae2-8e65-a410df84e7d1" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]", + "name": "[guid(variables('name'), variables('roleDefinitionId'), resourceGroup().id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitionId'))]", + "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('subscriptionId'), parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UAI_Artifacts_{0}', parameters('timestamp'))), '2022-09-01').outputs.principalId.value]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('subscriptionId'), parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UAI_Artifacts_{0}', parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('UAI_Artifacts_{0}', parameters('timestamp'))]", + "subscriptionId": "[parameters('subscriptionId')]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "name": { + "value": "[variables('name')]" + }, + "tags": "[if(contains(parameters('tags'), 'Microsoft.ManagedIdentity/userAssignedIdentities'), createObject('value', parameters('tags')['Microsoft.ManagedIdentity/userAssignedIdentities']), createObject('value', createObject()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "1968141880350840837" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2018-11-30", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "clientId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2018-11-30').clientId]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" + }, + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2018-11-30').principalId]" + } + } + } + } + } + ], + "outputs": { + "userAssignedIdentityClientId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('subscriptionId'), parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UAI_Artifacts_{0}', parameters('timestamp'))), '2022-09-01').outputs.clientId.value]" + }, + "userAssignedIdentityPrincipalId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('subscriptionId'), parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UAI_Artifacts_{0}', parameters('timestamp'))), '2022-09-01').outputs.principalId.value]" + }, + "userAssignedIdentityResourceId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('subscriptionId'), parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UAI_Artifacts_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceId.value]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('CustomerManagedKeys_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "environment": { + "value": "[parameters('environmentShortName')]" + }, + "keyVaultAbbreviation": { + "value": "[parameters('keyVaultAbbreviation')]" + }, + "keyVaultName": { + "value": "[parameters('keyVaultName')]" + }, + "keyVaultPrivateDnsZoneResourceId": { + "value": "[parameters('keyVaultPrivateDnsZoneResourceId')]" + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "subnetResourceId": { + "value": "[parameters('subnetResourceId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "userAssignedIdentityNamePrefix": { + "value": "[parameters('userAssignedIdentityNamePrefix')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "8624045572124550938" + } + }, + "parameters": { + "diskEncryptionKeyExpirationInDays": { + "type": "int", + "defaultValue": 30 + }, + "environment": { + "type": "string" + }, + "keyVaultAbbreviation": { + "type": "string" + }, + "keyVaultName": { + "type": "string" + }, + "keyVaultPrivateDnsZoneResourceId": { + "type": "string" + }, + "location": { + "type": "string" + }, + "subnetResourceId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "userAssignedIdentityNamePrefix": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2022-07-01", + "name": "[parameters('keyVaultName')]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.KeyVault/vaults'), parameters('tags')['Microsoft.KeyVault/vaults'], createObject())]", + "properties": { + "enabledForDeployment": false, + "enabledForDiskEncryption": true, + "enabledForTemplateDeployment": false, + "enablePurgeProtection": true, + "enableRbacAuthorization": true, + "enableSoftDelete": true, + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Deny", + "ipRules": [], + "virtualNetworkRules": [] + }, + "publicNetworkAccess": "Disabled", + "sku": { + "family": "A", + "name": "standard" + }, + "softDeleteRetentionInDays": "[if(or(equals(parameters('environment'), 'dev'), equals(parameters('environment'), 'test')), 7, 90)]", + "tenantId": "[subscription().tenantId]" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-04-01", + "name": "[replace(parameters('keyVaultName'), parameters('keyVaultAbbreviation'), format('{0}-pe', parameters('keyVaultAbbreviation')))]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.Network/privateEndpoints'), parameters('tags')['Microsoft.Network/privateEndpoints'], createObject())]", + "properties": { + "customNetworkInterfaceName": "[replace(parameters('keyVaultName'), parameters('keyVaultAbbreviation'), format('{0}-nic', parameters('keyVaultAbbreviation')))]", + "privateLinkServiceConnections": [ + { + "name": "[replace(parameters('keyVaultName'), parameters('keyVaultAbbreviation'), format('{0}-nic', parameters('keyVaultAbbreviation')))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]", + "groupIds": [ + "vault" + ] + } + } + ], + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-08-01", + "name": "[format('{0}/{1}', replace(parameters('keyVaultName'), parameters('keyVaultAbbreviation'), format('{0}-pe', parameters('keyVaultAbbreviation'))), parameters('keyVaultName'))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "ipconfig1", + "properties": { + "privateDnsZoneId": "[parameters('keyVaultPrivateDnsZoneResourceId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', replace(parameters('keyVaultName'), parameters('keyVaultAbbreviation'), format('{0}-pe', parameters('keyVaultAbbreviation'))))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2022-07-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'DiskEncryptionKey')]", + "properties": { + "attributes": { + "enabled": true + }, + "keySize": 4096, + "kty": "RSA", + "rotationPolicy": { + "attributes": { + "expiryTime": "[format('P{0}D', string(parameters('diskEncryptionKeyExpirationInDays')))]" + }, + "lifetimeActions": [ + { + "action": { + "type": "Notify" + }, + "trigger": { + "timeBeforeExpiry": "P10D" + } + }, + { + "action": { + "type": "Rotate" + }, + "trigger": { + "timeAfterCreate": "[format('P{0}D', string(sub(parameters('diskEncryptionKeyExpirationInDays'), 7)))]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2022-07-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'StorageEncryptionKey')]", + "properties": { + "attributes": { + "enabled": true + }, + "keySize": 4096, + "kty": "RSA", + "rotationPolicy": { + "attributes": { + "expiryTime": "[format('P{0}D', string(parameters('diskEncryptionKeyExpirationInDays')))]" + }, + "lifetimeActions": [ + { + "action": { + "type": "Notify" + }, + "trigger": { + "timeBeforeExpiry": "P10D" + } + }, + { + "action": { + "type": "Rotate" + }, + "trigger": { + "timeAfterCreate": "[format('P{0}D', string(sub(parameters('diskEncryptionKeyExpirationInDays'), 7)))]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('UAI_Encryption_{0}', parameters('timestamp'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "name": { + "value": "[format('{0}-encryption', parameters('userAssignedIdentityNamePrefix'))]" + }, + "tags": "[if(contains(parameters('tags'), 'Microsoft.ManagedIdentity/userAssignedIdentities'), createObject('value', parameters('tags')['Microsoft.ManagedIdentity/userAssignedIdentities']), createObject('value', createObject()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "1968141880350840837" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2018-11-30", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "clientId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2018-11-30').clientId]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" + }, + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2018-11-30').principalId]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('RoleAssignment_Encryption_{0}', parameters('timestamp'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "PrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('UAI_Encryption_{0}', parameters('timestamp'))), '2022-09-01').outputs.principalId.value]" + }, + "PrincipalType": { + "value": "ServicePrincipal" + }, + "RoleDefinitionId": { + "value": "e147488a-f6f5-4113-8e2d-b22465e65bf6" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "3999918654980032531" + } + }, + "parameters": { + "PrincipalId": { + "type": "string" + }, + "PrincipalType": { + "type": "string" + }, + "RoleDefinitionId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('PrincipalId'), parameters('RoleDefinitionId'), resourceGroup().id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('RoleDefinitionId'))]", + "principalId": "[parameters('PrincipalId')]", + "principalType": "[parameters('PrincipalType')]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('UAI_Encryption_{0}', parameters('timestamp')))]" + ] + } + ], + "outputs": { + "keyUriWithVersion": { + "type": "string", + "value": "[reference(resourceId('Microsoft.KeyVault/vaults/keys', parameters('keyVaultName'), 'DiskEncryptionKey'), '2022-07-01').keyUriWithVersion]" + }, + "keyVaultResourceId": { + "type": "string", + "value": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" + }, + "keyVaultUri": { + "type": "string", + "value": "[reference(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), '2022-07-01').vaultUri]" + }, + "storageKeyName": { + "type": "string", + "value": "StorageEncryptionKey" + }, + "encryptionUserAssignedIdentityClientId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('UAI_Encryption_{0}', parameters('timestamp'))), '2022-09-01').outputs.clientId.value]" + }, + "encryptionUserAssignedIdentityPrincipalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('UAI_Encryption_{0}', parameters('timestamp'))), '2022-09-01').outputs.principalId.value]" + }, + "encryptionUserAssignedIdentityResourceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('UAI_Encryption_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceId.value]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('DiskEncryptionSet_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "diskEncryptionSetName": { + "value": "[parameters('diskEncryptionSetName')]" + }, + "keyUrl": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('CustomerManagedKeys_{0}', parameters('timestamp'))), '2022-09-01').outputs.keyUriWithVersion.value]" + }, + "keyVaultResourceId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('CustomerManagedKeys_{0}', parameters('timestamp'))), '2022-09-01').outputs.keyVaultResourceId.value]" + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "tags": "[if(contains(parameters('tags'), 'Microsoft.Compute/diskEncryptionSets'), createObject('value', parameters('tags')['Microsoft.Compute/diskEncryptionSets']), createObject('value', createObject()))]", + "timestamp": { + "value": "[parameters('timestamp')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "17052545534600497369" + } + }, + "parameters": { + "diskEncryptionSetName": { + "type": "string" + }, + "keyVaultResourceId": { + "type": "string" + }, + "keyUrl": { + "type": "string" + }, + "location": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/diskEncryptionSets", + "apiVersion": "2023-04-02", + "name": "[parameters('diskEncryptionSetName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "activeKey": { + "sourceVault": { + "id": "[parameters('keyVaultResourceId')]" + }, + "keyUrl": "[parameters('keyUrl')]" + }, + "encryptionType": "EncryptionAtRestWithPlatformAndCustomerKeys", + "rotationToLatestKeyVersionEnabled": true + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('RoleAssignment_Encryption_{0}', parameters('timestamp'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "PrincipalId": { + "value": "[reference(resourceId('Microsoft.Compute/diskEncryptionSets', parameters('diskEncryptionSetName')), '2023-04-02', 'full').identity.principalId]" + }, + "PrincipalType": { + "value": "ServicePrincipal" + }, + "RoleDefinitionId": { + "value": "e147488a-f6f5-4113-8e2d-b22465e65bf6" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "3999918654980032531" + } + }, + "parameters": { + "PrincipalId": { + "type": "string" + }, + "PrincipalType": { + "type": "string" + }, + "RoleDefinitionId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('PrincipalId'), parameters('RoleDefinitionId'), resourceGroup().id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('RoleDefinitionId'))]", + "principalId": "[parameters('PrincipalId')]", + "principalType": "[parameters('PrincipalType')]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/diskEncryptionSets', parameters('diskEncryptionSetName'))]" + ] + } + ], + "outputs": { + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.Compute/diskEncryptionSets', parameters('diskEncryptionSetName'))]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('CustomerManagedKeys_{0}', parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('ManagementVirtualMachine_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "artifactsUri": { + "value": "[parameters('artifactsUri')]" + }, + "azurePowerShellModuleMsiName": { + "value": "[parameters('azurePowerShellModuleMsiName')]" + }, + "deploymentUserAssignedIdentityClientId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp'))), '2022-09-01').outputs.clientId.value]" + }, + "deploymentUserAssignedIdentityResourceId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceId.value]" + }, + "diskEncryptionSetResourceId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('DiskEncryptionSet_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceId.value]" + }, + "diskNamePrefix": { + "value": "[parameters('diskNamePrefix')]" + }, + "diskSku": { + "value": "[parameters('diskSku')]" + }, + "domainJoinPassword": { + "value": "[parameters('domainJoinPassword')]" + }, + "domainJoinUserPrincipalName": { + "value": "[parameters('domainJoinUserPrincipalName')]" + }, + "domainName": { + "value": "[parameters('domainName')]" + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "networkInterfaceNamePrefix": { + "value": "[parameters('networkInterfaceNamePrefix')]" + }, + "organizationalUnitPath": { + "value": "[parameters('organizationalUnitPath')]" + }, + "securityLogAnalyticsWorkspaceResourceId": { + "value": "[parameters('securityLogAnalyticsWorkspaceResourceId')]" + }, + "subnet": { + "value": "[split(parameters('subnetResourceId'), '/')[10]]" + }, + "tagsNetworkInterfaces": "[if(contains(parameters('tags'), 'Microsoft.Network/networkInterfaces'), createObject('value', parameters('tags')['Microsoft.Network/networkInterfaces']), createObject('value', createObject()))]", + "tagsVirtualMachines": "[if(contains(parameters('tags'), 'Microsoft.Compute/virtualMachines'), createObject('value', parameters('tags')['Microsoft.Compute/virtualMachines']), createObject('value', createObject()))]", + "virtualMachineMonitoringAgent": { + "value": "[parameters('virtualMachineMonitoringAgent')]" + }, + "virtualMachineNamePrefix": { + "value": "[parameters('virtualMachineNamePrefix')]" + }, + "virtualMachinePassword": { + "value": "[parameters('virtualMachinePassword')]" + }, + "virtualMachineUsername": { + "value": "[parameters('virtualMachineUsername')]" + }, + "virtualNetwork": { + "value": "[variables('VirtualNetworkName')]" + }, + "virtualNetworkResourceGroup": { + "value": "[variables('VirtualNetworkResourceGroupName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "1082833111773432320" + } + }, + "parameters": { + "artifactsUri": { + "type": "string" + }, + "azurePowerShellModuleMsiName": { + "type": "string" + }, + "deploymentUserAssignedIdentityClientId": { + "type": "string" + }, + "deploymentUserAssignedIdentityResourceId": { + "type": "string" + }, + "diskEncryptionSetResourceId": { + "type": "string" + }, + "diskNamePrefix": { + "type": "string" + }, + "diskSku": { + "type": "string" + }, + "domainJoinPassword": { + "type": "securestring" + }, + "domainJoinUserPrincipalName": { + "type": "string" + }, + "domainName": { + "type": "string" + }, + "location": { + "type": "string" + }, + "networkInterfaceNamePrefix": { + "type": "string" + }, + "organizationalUnitPath": { + "type": "string" + }, + "securityLogAnalyticsWorkspaceResourceId": { + "type": "string" + }, + "subnet": { + "type": "string" + }, + "tagsNetworkInterfaces": { + "type": "object" + }, + "tagsVirtualMachines": { + "type": "object" + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]" + }, + "virtualNetwork": { + "type": "string" + }, + "virtualNetworkResourceGroup": { + "type": "string" + }, + "virtualMachineMonitoringAgent": { + "type": "string" + }, + "virtualMachineNamePrefix": { + "type": "string" + }, + "virtualMachinePassword": { + "type": "securestring" + }, + "virtualMachineUsername": { + "type": "string" + } + }, + "variables": { + "networkInterfaceName": "[format('{0}mgt', parameters('networkInterfaceNamePrefix'))]", + "securitylogAnalyticsWorkspaceName": "[if(variables('securityMonitoring'), split(parameters('securityLogAnalyticsWorkspaceResourceId'), '/')[8], '')]", + "securityLogAnalyticsWorkspaceResourceGroupName": "[if(variables('securityMonitoring'), split(parameters('securityLogAnalyticsWorkspaceResourceId'), '/')[4], resourceGroup().name)]", + "securityLogAnalyticsWorkspaceSubscriptionId": "[if(variables('securityMonitoring'), split(parameters('securityLogAnalyticsWorkspaceResourceId'), '/')[2], subscription().subscriptionId)]", + "securityMonitoring": "[if(empty(parameters('securityLogAnalyticsWorkspaceResourceId')), false(), true())]", + "virtualMachineName": "[format('{0}mgt', parameters('virtualMachineNamePrefix'))]" + }, + "resources": [ + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2020-05-01", + "name": "[variables('networkInterfaceName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsNetworkInterfaces')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[resourceId(parameters('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetwork'), parameters('subnet'))]" + }, + "primary": true, + "privateIPAddressVersion": "IPv4" + } + } + ], + "enableAcceleratedNetworking": false, + "enableIPForwarding": false + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2021-11-01", + "name": "[variables('virtualMachineName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B2s" + }, + "storageProfile": { + "imageReference": { + "publisher": "MicrosoftWindowsServer", + "offer": "WindowsServer", + "sku": "2019-datacenter-core-g2", + "version": "latest" + }, + "osDisk": { + "deleteOption": "Delete", + "osType": "Windows", + "createOption": "FromImage", + "caching": "None", + "managedDisk": { + "diskEncryptionSet": { + "id": "[parameters('diskEncryptionSetResourceId')]" + }, + "storageAccountType": "[parameters('diskSku')]" + }, + "name": "[format('{0}mgt', parameters('diskNamePrefix'))]" + }, + "dataDisks": [] + }, + "osProfile": { + "computerName": "[variables('virtualMachineName')]", + "adminUsername": "[parameters('virtualMachineUsername')]", + "adminPassword": "[parameters('virtualMachinePassword')]", + "windowsConfiguration": { + "provisionVMAgent": true, + "enableAutomaticUpdates": false + }, + "secrets": [], + "allowExtensionOperations": true + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]", + "properties": { + "deleteOption": "Delete" + } + } + ] + }, + "securityProfile": { + "uefiSettings": { + "secureBootEnabled": true, + "vTpmEnabled": true + }, + "securityType": "TrustedLaunch", + "encryptionAtHost": true + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "licenseType": "Windows_Server" + }, + "identity": { + "type": "SystemAssigned, UserAssigned", + "userAssignedIdentities": { + "[format('{0}', parameters('deploymentUserAssignedIdentityResourceId'))]": {} + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', variables('virtualMachineName'), 'IaaSAntimalware')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "publisher": "Microsoft.Azure.Security", + "type": "IaaSAntimalware", + "typeHandlerVersion": "1.3", + "autoUpgradeMinorVersion": true, + "enableAutomaticUpgrade": false, + "settings": { + "AntimalwareEnabled": true, + "RealtimeProtectionEnabled": "true", + "ScheduledScanSettings": { + "isEnabled": "true", + "day": "7", + "time": "120", + "scanType": "Quick" + }, + "Exclusions": {} + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', variables('virtualMachineName'))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', variables('virtualMachineName'), 'GuestAttestation')]", + "location": "[parameters('location')]", + "properties": { + "publisher": "Microsoft.Azure.Security.WindowsAttestation", + "type": "GuestAttestation", + "typeHandlerVersion": "1.0", + "autoUpgradeMinorVersion": true, + "settings": { + "AttestationConfig": { + "MaaSettings": { + "maaEndpoint": "", + "maaTenantName": "GuestAttestation" + }, + "AscSettings": { + "ascReportingEndpoint": "", + "ascReportingFrequency": "" + }, + "useCustomToken": "false", + "disableAlerts": "false" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', variables('virtualMachineName'))]" + ] + }, + { + "condition": "[and(variables('securityMonitoring'), equals(parameters('virtualMachineMonitoringAgent'), 'LogAnalyticsAgent'))]", + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', variables('virtualMachineName'), 'MicrosoftmonitoringAgent')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "publisher": "Microsoft.EnterpriseCloud.monitoring", + "type": "MicrosoftmonitoringAgent", + "typeHandlerVersion": "1.0", + "autoUpgradeMinorVersion": true, + "settings": { + "workspaceId": "[if(variables('securityMonitoring'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('securityLogAnalyticsWorkspaceSubscriptionId'), variables('securityLogAnalyticsWorkspaceResourceGroupName')), 'Microsoft.OperationalInsights/workspaces', variables('securitylogAnalyticsWorkspaceName')), '2022-10-01').customerId, null())]" + }, + "protectedSettings": { + "workspaceKey": "[if(variables('securityMonitoring'), listKeys(parameters('securityLogAnalyticsWorkspaceResourceId'), '2021-06-01').primarySharedKey, null())]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines/extensions', variables('virtualMachineName'), 'IaaSAntimalware')]", + "[resourceId('Microsoft.Compute/virtualMachines', variables('virtualMachineName'))]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2019-07-01", + "name": "[format('{0}/{1}', variables('virtualMachineName'), 'JsonADDomainExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "forceUpdateTag": "[parameters('timestamp')]", + "publisher": "Microsoft.Compute", + "type": "JsonADDomainExtension", + "typeHandlerVersion": "1.3", + "autoUpgradeMinorVersion": true, + "settings": { + "Name": "[parameters('domainName')]", + "Options": "3", + "OUPath": "[parameters('organizationalUnitPath')]", + "Restart": "true", + "User": "[parameters('domainJoinUserPrincipalName')]" + }, + "protectedSettings": { + "Password": "[parameters('domainJoinPassword')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('CSE_InstallAzurePowerShellAzModule_{0}', parameters('timestamp')))]", + "[resourceId('Microsoft.Compute/virtualMachines', variables('virtualMachineName'))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('CSE_InstallAzurePowerShellAzModule_{0}', parameters('timestamp'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileUris": { + "value": [ + "[format('{0}{1}', parameters('artifactsUri'), parameters('azurePowerShellModuleMsiName'))]", + "[format('{0}Install-AzurePowerShellAzModule.ps1', parameters('artifactsUri'))]" + ] + }, + "location": { + "value": "[parameters('location')]" + }, + "parameters": { + "value": "[format('-Installer {0}', parameters('azurePowerShellModuleMsiName'))]" + }, + "scriptFileName": { + "value": "Install-AzurePowerShellAzModule.ps1" + }, + "tags": { + "value": "[parameters('tagsVirtualMachines')]" + }, + "virtualMachineName": { + "value": "[variables('virtualMachineName')]" + }, + "userAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12496195847910288876" + } + }, + "parameters": { + "fileUris": { + "type": "array" + }, + "location": { + "type": "string" + }, + "parameters": { + "type": "securestring" + }, + "scriptFileName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]" + }, + "userAssignedIdentityClientId": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), 'CustomScriptExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "timestamp": "[parameters('timestamp')]" + }, + "protectedSettings": { + "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File {0} {1}', parameters('scriptFileName'), parameters('parameters'))]", + "fileUris": "[parameters('fileUris')]", + "managedIdentity": { + "clientId": "[parameters('userAssignedIdentityClientId')]" + } + } + } + } + ], + "outputs": { + "value": { + "type": "object", + "value": "[json(filter(reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'CustomScriptExtension'), '2021-03-01').instanceView.substatuses, lambda('item', equals(lambdaVariables('item').code, 'ComponentStatus/StdOut/succeeded')))[0].message)]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines/extensions', variables('virtualMachineName'), 'MicrosoftmonitoringAgent')]", + "[resourceId('Microsoft.Compute/virtualMachines', variables('virtualMachineName'))]" + ] + } + ], + "outputs": { + "Name": { + "type": "string", + "value": "[variables('virtualMachineName')]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('DiskEncryptionSet_{0}', parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Validations_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileUris": { + "value": [ + "[format('{0}Get-Validations.ps1', parameters('artifactsUri'))]" + ] + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "parameters": { + "value": "[format('-ActiveDirectorySolution {0} -CpuCountMax {1} -CpuCountMin {2} -DomainName {3} -Environment {4} -ImageDefinitionResourceId {5} -Location {6} -SessionHostCount {7} -StorageService {8} -SubscriptionId {9} -TenantId {10} -UserAssignedIdentityClientId {11} -VirtualMachineSize {12} -VirtualNetworkName {13} -VirtualNetworkResourceGroupName {14} -WorkspaceNamePrefix {15} -WorkspaceResourceGroupName {16}', parameters('activeDirectorySolution'), variables('CpuCountMax'), variables('CpuCountMin'), if(empty(parameters('domainName')), 'NotApplicable', parameters('domainName')), environment().name, if(empty(parameters('imageDefinitionResourceId')), 'NotApplicable', parameters('imageDefinitionResourceId')), parameters('locationVirtualMachines'), parameters('sessionHostCount'), parameters('storageService'), subscription().subscriptionId, tenant().tenantId, reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp'))), '2022-09-01').outputs.clientId.value, parameters('virtualMachineSize'), variables('VirtualNetworkName'), variables('VirtualNetworkResourceGroupName'), parameters('workspaceNamePrefix'), parameters('resourceGroupFeedWorkspace'))]" + }, + "scriptFileName": { + "value": "Get-Validations.ps1" + }, + "tags": "[if(contains(parameters('tags'), 'Microsoft.Compute/virtualMachines'), createObject('value', parameters('tags')['Microsoft.Compute/virtualMachines']), createObject('value', createObject()))]", + "userAssignedIdentityClientId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp'))), '2022-09-01').outputs.clientId.value]" + }, + "virtualMachineName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('ManagementVirtualMachine_{0}', parameters('timestamp'))), '2022-09-01').outputs.Name.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12496195847910288876" + } + }, + "parameters": { + "fileUris": { + "type": "array" + }, + "location": { + "type": "string" + }, + "parameters": { + "type": "securestring" + }, + "scriptFileName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]" + }, + "userAssignedIdentityClientId": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), 'CustomScriptExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "timestamp": "[parameters('timestamp')]" + }, + "protectedSettings": { + "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File {0} {1}', parameters('scriptFileName'), parameters('parameters'))]", + "fileUris": "[parameters('fileUris')]", + "managedIdentity": { + "clientId": "[parameters('userAssignedIdentityClientId')]" + } + } + } + } + ], + "outputs": { + "value": { + "type": "object", + "value": "[json(filter(reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'CustomScriptExtension'), '2021-03-01').instanceView.substatuses, lambda('item', equals(lambdaVariables('item').code, 'ComponentStatus/StdOut/succeeded')))[0].message)]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('ManagementVirtualMachine_{0}', parameters('timestamp')))]" + ] + }, + { + "condition": "[parameters('enableMonitoring')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Monitoring_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "dataCollectionRuleName": { + "value": "[parameters('dataCollectionRuleName')]" + }, + "hostPoolName": { + "value": "[parameters('hostPoolName')]" + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "logAnalyticsWorkspaceName": { + "value": "[parameters('logAnalyticsWorkspaceName')]" + }, + "logAnalyticsWorkspaceRetention": { + "value": "[parameters('logAnalyticsWorkspaceRetention')]" + }, + "logAnalyticsWorkspaceSku": { + "value": "[parameters('logAnalyticsWorkspaceSku')]" + }, + "resourceGroupControlPlane": { + "value": "[parameters('resourceGroupControlPlane')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "virtualMachineMonitoringAgent": { + "value": "[parameters('virtualMachineMonitoringAgent')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "3361222361094221028" + } + }, + "parameters": { + "dataCollectionRuleName": { + "type": "string" + }, + "hostPoolName": { + "type": "string" + }, + "location": { + "type": "string" + }, + "logAnalyticsWorkspaceName": { + "type": "string" + }, + "logAnalyticsWorkspaceRetention": { + "type": "int" + }, + "logAnalyticsWorkspaceSku": { + "type": "string" + }, + "resourceGroupControlPlane": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "virtualMachineMonitoringAgent": { + "type": "string" + } + }, + "variables": { + "WindowsEvents": [ + { + "name": "Microsoft-FSLogix-Apps/Operational", + "types": [ + { + "eventType": "Error" + }, + { + "eventType": "Warning" + }, + { + "eventType": "Information" + } + ] + }, + { + "name": "Microsoft-Windows-TerminalServices-LocalSessionManager/Operational", + "types": [ + { + "eventType": "Error" + }, + { + "eventType": "Warning" + }, + { + "eventType": "Information" + } + ] + }, + { + "name": "System", + "types": [ + { + "eventType": "Error" + }, + { + "eventType": "Warning" + } + ] + }, + { + "name": "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Admin", + "types": [ + { + "eventType": "Error" + }, + { + "eventType": "Warning" + }, + { + "eventType": "Information" + } + ] + }, + { + "name": "Microsoft-FSLogix-Apps/Admin", + "types": [ + { + "eventType": "Error" + }, + { + "eventType": "Warning" + }, + { + "eventType": "Information" + } + ] + }, + { + "name": "Application", + "types": [ + { + "eventType": "Error" + }, + { + "eventType": "Warning" + } + ] + } + ], + "WindowsPerformanceCounters": [ + { + "objectName": "LogicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Disk Transfers/sec" + }, + { + "objectName": "LogicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Current Disk Queue Length" + }, + { + "objectName": "LogicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Disk Reads/sec" + }, + { + "objectName": "LogicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "% Free Space" + }, + { + "objectName": "LogicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Avg. Disk sec/Read" + }, + { + "objectName": "LogicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Disk Writes/sec" + }, + { + "objectName": "LogicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Avg. Disk sec/Write" + }, + { + "objectName": "LogicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Free Megabytes" + }, + { + "objectName": "LogicalDisk", + "instanceName": "C:", + "intervalSeconds": 60, + "counterName": "% Free Space" + }, + { + "objectName": "LogicalDisk", + "instanceName": "C:", + "intervalSeconds": 30, + "counterName": "Avg. Disk Queue Length" + }, + { + "objectName": "LogicalDisk", + "instanceName": "C:", + "intervalSeconds": 60, + "counterName": "Avg. Disk sec/Transfer" + }, + { + "objectName": "LogicalDisk", + "instanceName": "C:", + "intervalSeconds": 30, + "counterName": "Current Disk Queue Length" + }, + { + "objectName": "Memory", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "% Committed Bytes In Use" + }, + { + "objectName": "Memory", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Available MBytes" + }, + { + "objectName": "Memory", + "instanceName": "*", + "intervalSeconds": 30, + "counterName": "Available Mbytes" + }, + { + "objectName": "Memory", + "instanceName": "*", + "intervalSeconds": 30, + "counterName": "Page Faults/sec" + }, + { + "objectName": "Memory", + "instanceName": "*", + "intervalSeconds": 30, + "counterName": "Pages/sec" + }, + { + "objectName": "Network Adapter", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Bytes Sent/sec" + }, + { + "objectName": "Network Adapter", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Bytes Received/sec" + }, + { + "objectName": "Network Interface", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Bytes Total/sec" + }, + { + "objectName": "PhysicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Avg. Disk Bytes/Transfer" + }, + { + "objectName": "PhysicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Avg. Disk Bytes/Read" + }, + { + "objectName": "PhysicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Avg. Disk sec/Write" + }, + { + "objectName": "PhysicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Avg. Disk sec/Read" + }, + { + "objectName": "PhysicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Avg. Disk Bytes/Write" + }, + { + "objectName": "PhysicalDisk", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Avg. Disk sec/Transfer" + }, + { + "objectName": "PhysicalDisk", + "instanceName": "*", + "intervalSeconds": 30, + "counterName": "Avg. Disk Queue Length" + }, + { + "objectName": "Process", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "IO Write Operations/sec" + }, + { + "objectName": "Process", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "IO Read Operations/sec" + }, + { + "objectName": "Process", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Thread Count" + }, + { + "objectName": "Process", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "% User Time" + }, + { + "objectName": "Process", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Working Set" + }, + { + "objectName": "Process", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "% Processor Time" + }, + { + "objectName": "Processor", + "instanceName": "_Total", + "intervalSeconds": 60, + "counterName": "% Processor Time" + }, + { + "objectName": "Processor Information", + "instanceName": "_Total", + "intervalSeconds": 30, + "counterName": "% Processor Time" + }, + { + "objectName": "RemoteFX Graphics", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Frames Skipped/Second - Insufficient Server Resources" + }, + { + "objectName": "RemoteFX Graphics", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Average Encoding Time" + }, + { + "objectName": "RemoteFX Graphics", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Frames Skipped/Second - Insufficient Client Resources" + }, + { + "objectName": "RemoteFX Graphics", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Frames Skipped/Second - Insufficient Network Resources" + }, + { + "objectName": "RemoteFX Network", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Current UDP Bandwidth" + }, + { + "objectName": "RemoteFX Network", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Current TCP Bandwidth" + }, + { + "objectName": "RemoteFX Network", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Current TCP RTT" + }, + { + "objectName": "RemoteFX Network", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Current UDP RTT" + }, + { + "objectName": "System", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Processor Queue Length" + }, + { + "objectName": "Terminal Services", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Inactive Sessions" + }, + { + "objectName": "Terminal Services", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Total Sessions" + }, + { + "objectName": "Terminal Services", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "Active Sessions" + }, + { + "objectName": "Terminal Services Session", + "instanceName": "*", + "intervalSeconds": 60, + "counterName": "% Processor Time" + }, + { + "objectName": "User Input Delay per Process", + "instanceName": "*", + "intervalSeconds": 30, + "counterName": "Max Input Delay" + }, + { + "objectName": "User Input Delay per Session", + "instanceName": "*", + "intervalSeconds": 30, + "counterName": "Max Input Delay" + } + ] + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2021-06-01", + "name": "[parameters('logAnalyticsWorkspaceName')]", + "location": "[parameters('location')]", + "tags": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupControlPlane'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.OperationalInsights/workspaces'), parameters('tags')['Microsoft.OperationalInsights/workspaces'], createObject()))]", + "properties": { + "sku": { + "name": "[parameters('logAnalyticsWorkspaceSku')]" + }, + "retentionInDays": "[parameters('logAnalyticsWorkspaceRetention')]", + "workspaceCapping": { + "dailyQuotaGb": -1 + }, + "publicNetworkAccessForIngestion": "Enabled", + "publicNetworkAccessForQuery": "Enabled" + } + }, + { + "copy": { + "name": "windowsEvents", + "count": "[length(variables('WindowsEvents'))]", + "mode": "serial", + "batchSize": 1 + }, + "condition": "[equals(parameters('virtualMachineMonitoringAgent'), 'LogAnalyticsAgent')]", + "type": "Microsoft.OperationalInsights/workspaces/dataSources", + "apiVersion": "2020-08-01", + "name": "[format('{0}/{1}', parameters('logAnalyticsWorkspaceName'), format('WindowsEvent{0}', copyIndex()))]", + "tags": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupControlPlane'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.OperationalInsights/workspaces'), parameters('tags')['Microsoft.OperationalInsights/workspaces'], createObject()))]", + "kind": "WindowsEvent", + "properties": { + "eventLogName": "[variables('WindowsEvents')[copyIndex()].name]", + "eventTypes": "[variables('WindowsEvents')[copyIndex()].types]" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceName'))]" + ] + }, + { + "copy": { + "name": "windowsPerformanceCounters", + "count": "[length(variables('WindowsPerformanceCounters'))]", + "mode": "serial", + "batchSize": 1 + }, + "condition": "[equals(parameters('virtualMachineMonitoringAgent'), 'LogAnalyticsAgent')]", + "type": "Microsoft.OperationalInsights/workspaces/dataSources", + "apiVersion": "2020-08-01", + "name": "[format('{0}/{1}', parameters('logAnalyticsWorkspaceName'), format('WindowsPerformanceCounter{0}', copyIndex()))]", + "tags": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupControlPlane'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.OperationalInsights/workspaces'), parameters('tags')['Microsoft.OperationalInsights/workspaces'], createObject()))]", + "kind": "WindowsPerformanceCounter", + "properties": { + "objectName": "[variables('WindowsPerformanceCounters')[copyIndex()].objectName]", + "instanceName": "[variables('WindowsPerformanceCounters')[copyIndex()].instanceName]", + "intervalSeconds": "[variables('WindowsPerformanceCounters')[copyIndex()].intervalSeconds]", + "counterName": "[variables('WindowsPerformanceCounters')[copyIndex()].counterName]" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceName'))]", + "windowsEvents" + ] + }, + { + "condition": "[equals(parameters('virtualMachineMonitoringAgent'), 'AzureMonitorAgent')]", + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2022-06-01", + "name": "[parameters('dataCollectionRuleName')]", + "location": "[parameters('location')]", + "tags": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupControlPlane'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Insights/dataCollectionRules'), parameters('tags')['Microsoft.Insights/dataCollectionRules'], createObject()))]", + "kind": "Windows", + "properties": { + "dataSources": { + "performanceCounters": [ + { + "streams": [ + "Microsoft-Perf" + ], + "samplingFrequencyInSeconds": 30, + "counterSpecifiers": [ + "\\LogicalDisk(C:)\\Avg. Disk Queue Length", + "\\LogicalDisk(C:)\\Current Disk Queue Length", + "\\Memory\\Available Mbytes", + "\\Memory\\Page Faults/sec", + "\\Memory\\Pages/sec", + "\\Memory\\% Committed Bytes In Use", + "\\PhysicalDisk(*)\\Avg. Disk Queue Length", + "\\PhysicalDisk(*)\\Avg. Disk sec/Read", + "\\PhysicalDisk(*)\\Avg. Disk sec/Transfer", + "\\PhysicalDisk(*)\\Avg. Disk sec/Write", + "\\Processor Information(_Total)\\% Processor Time", + "\\User Input Delay per Process(*)\\Max Input Delay", + "\\User Input Delay per Session(*)\\Max Input Delay", + "\\RemoteFX Network(*)\\Current TCP RTT", + "\\RemoteFX Network(*)\\Current UDP Bandwidth" + ], + "name": "perfCounterDataSource10" + }, + { + "streams": [ + "Microsoft-Perf" + ], + "samplingFrequencyInSeconds": 60, + "counterSpecifiers": [ + "\\LogicalDisk(C:)\\% Free Space", + "\\LogicalDisk(C:)\\Avg. Disk sec/Transfer", + "\\Terminal Services(*)\\Active Sessions", + "\\Terminal Services(*)\\Inactive Sessions", + "\\Terminal Services(*)\\Total Sessions" + ], + "name": "perfCounterDataSource30" + } + ], + "windowsEventLogs": [ + { + "streams": [ + "Microsoft-Event" + ], + "xPathQueries": [ + "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Admin!*[System[(Level=2 or Level=3 or Level=4 or Level=0)]]", + "Microsoft-Windows-TerminalServices-LocalSessionManager/Operational!*[System[(Level=2 or Level=3 or Level=4 or Level=0)]]", + "System!*", + "Microsoft-FSLogix-Apps/Operational!*[System[(Level=2 or Level=3 or Level=4 or Level=0)]]", + "Application!*[System[(Level=2 or Level=3)]]", + "Microsoft-FSLogix-Apps/Admin!*[System[(Level=2 or Level=3 or Level=4 or Level=0)]]" + ], + "name": "eventLogsDataSource" + } + ] + }, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceName'))]", + "name": "la-workspace" + } + ] + }, + "dataFlows": [ + { + "streams": [ + "Microsoft-Perf", + "Microsoft-Event" + ], + "destinations": [ + "la-workspace" + ] + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceName'))]" + ] + } + ], + "outputs": { + "logAnalyticsWorkspaceResourceId": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceName'))]" + }, + "dataCollectionRuleResourceId": { + "type": "string", + "value": "[if(equals(parameters('virtualMachineMonitoringAgent'), 'AzureMonitorAgent'), resourceId('Microsoft.Insights/dataCollectionRules', parameters('dataCollectionRuleName')), '')]" + } + } + } + } + }, + { + "condition": "[or(parameters('scalingTool'), equals(parameters('fslogixStorageService'), 'AzureFiles Premium'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('AutomationAccount_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "automationAccountName": { + "value": "[parameters('automationAccountName')]" + }, + "automationAccountPrivateDnsZoneResourceId": { + "value": "[parameters('automationAccountPrivateDnsZoneResourceId')]" + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "logAnalyticsWorkspaceResourceId": "[if(parameters('enableMonitoring'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('Monitoring_{0}', parameters('timestamp'))), '2022-09-01').outputs.logAnalyticsWorkspaceResourceId.value), createObject('value', ''))]", + "monitoring": { + "value": "[parameters('enableMonitoring')]" + }, + "subnetResourceId": { + "value": "[parameters('subnetResourceId')]" + }, + "tags": "[if(contains(parameters('tags'), 'Microsoft.Automation/automationAccounts'), createObject('value', parameters('tags')['Microsoft.Automation/automationAccounts']), createObject('value', createObject()))]", + "virtualMachineName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('ManagementVirtualMachine_{0}', parameters('timestamp'))), '2022-09-01').outputs.Name.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "1888008991553556693" + } + }, + "parameters": { + "automationAccountName": { + "type": "string" + }, + "automationAccountPrivateDnsZoneResourceId": { + "type": "string" + }, + "location": { + "type": "string" + }, + "logAnalyticsWorkspaceResourceId": { + "type": "string" + }, + "monitoring": { + "type": "bool" + }, + "subnetResourceId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "virtualMachineName": { + "type": "string" + } + }, + "variables": { + "privateEndpointName": "[format('pe-{0}-DSCAndHybridWorker', parameters('automationAccountName'))]" + }, + "resources": [ + { + "type": "Microsoft.Automation/automationAccounts", + "apiVersion": "2021-06-22", + "name": "[parameters('automationAccountName')]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.Automation/automationAccounts'), parameters('tags')['Microsoft.Automation/automationAccounts'], createObject())]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "sku": { + "name": "Free" + } + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-04-01", + "name": "[variables('privateEndpointName')]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.Network/privateEndpoints'), parameters('tags')['Microsoft.Network/privateEndpoints'], createObject())]", + "properties": { + "customNetworkInterfaceName": "[format('nic-{0}-DSCAndHybridWorker', parameters('automationAccountName'))]", + "privateLinkServiceConnections": [ + { + "name": "[variables('privateEndpointName')]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]", + "groupIds": [ + "DSCAndHybridWorker" + ] + } + } + ], + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', variables('privateEndpointName'), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[replace(split(parameters('automationAccountPrivateDnsZoneResourceId'), '/')[8], '.', '-')]", + "properties": { + "privateDnsZoneId": "[parameters('automationAccountPrivateDnsZoneResourceId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('privateEndpointName'))]" + ] + }, + { + "type": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups", + "apiVersion": "2022-08-08", + "name": "[format('{0}/{1}', parameters('automationAccountName'), 'Scaling Tool')]", + "dependsOn": [ + "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]" + ] + }, + { + "type": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/hybridRunbookWorkers", + "apiVersion": "2022-08-08", + "name": "[format('{0}/{1}/{2}', parameters('automationAccountName'), 'Scaling Tool', guid(resourceId('Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups', parameters('automationAccountName'), 'Scaling Tool')))]", + "properties": { + "vmResourceId": "[resourceId('Microsoft.Compute/virtualMachines', parameters('virtualMachineName'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups', parameters('automationAccountName'), 'Scaling Tool')]" + ] + }, + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2022-03-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), 'HybridWorkerForWindows')]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.Compute/virtualMachines'), parameters('tags')['Microsoft.Compute/virtualMachines'], createObject())]", + "properties": { + "publisher": "Microsoft.Azure.Automation.HybridWorker", + "type": "HybridWorkerForWindows", + "typeHandlerVersion": "1.1", + "autoUpgradeMinorVersion": true, + "enableAutomaticUpgrade": true, + "settings": { + "AutomationAccountURL": "[reference(resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName')), '2021-06-22').automationHybridServiceUrl]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]" + ] + }, + { + "condition": "[parameters('monitoring')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2017-05-01-preview", + "scope": "[format('Microsoft.Automation/automationAccounts/{0}', parameters('automationAccountName'))]", + "name": "[format('diag-{0}', parameters('automationAccountName'))]", + "properties": { + "logs": [ + { + "category": "DscNodeStatus", + "enabled": true + }, + { + "category": "JobLogs", + "enabled": true + }, + { + "category": "JobStreams", + "enabled": true + } + ], + "workspaceId": "[parameters('logAnalyticsWorkspaceResourceId')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName'))]" + ] + } + ], + "outputs": { + "hybridRunbookWorkerGroupName": { + "type": "string", + "value": "Scaling Tool" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('Monitoring_{0}', parameters('timestamp')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('ManagementVirtualMachine_{0}', parameters('timestamp')))]" + ] + }, + { + "condition": "[and(parameters('recoveryServices'), or(and(and(contains(parameters('activeDirectorySolution'), 'DomainServices'), contains(parameters('hostPoolType'), 'Pooled')), contains(parameters('fslogixStorageService'), 'AzureFiles')), contains(parameters('hostPoolType'), 'Personal')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('RecoveryServicesVault_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "azureBlobsPrivateDnsZoneResourceId": { + "value": "[parameters('azureBlobsPrivateDnsZoneResourceId')]" + }, + "fslogix": { + "value": "[parameters('fslogix')]" + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "azureQueueStoragePrivateDnsZoneResourceId": { + "value": "[parameters('azureQueueStoragePrivateDnsZoneResourceId')]" + }, + "recoveryServicesPrivateDnsZoneResourceId": { + "value": "[parameters('recoveryServicesPrivateDnsZoneResourceId')]" + }, + "recoveryServicesVaultName": { + "value": "[parameters('recoveryServicesVaultName')]" + }, + "storageService": { + "value": "[parameters('storageService')]" + }, + "subnetId": { + "value": "[parameters('subnetResourceId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "timeZone": { + "value": "[parameters('timeZone')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "17055881954608987962" + } + }, + "parameters": { + "azureBlobsPrivateDnsZoneResourceId": { + "type": "string" + }, + "azureQueueStoragePrivateDnsZoneResourceId": { + "type": "string" + }, + "fslogix": { + "type": "bool" + }, + "location": { + "type": "string" + }, + "recoveryServicesPrivateDnsZoneResourceId": { + "type": "string" + }, + "recoveryServicesVaultName": { + "type": "string" + }, + "storageService": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timeZone": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.RecoveryServices/vaults", + "apiVersion": "2022-03-01", + "name": "[parameters('recoveryServicesVaultName')]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.RecoveryServices/vaults'), parameters('tags')['Microsoft.RecoveryServices/vaults'], createObject())]", + "sku": { + "name": "RS0", + "tier": "Standard" + }, + "properties": {} + }, + { + "condition": "[and(parameters('fslogix'), equals(parameters('storageService'), 'AzureFiles'))]", + "type": "Microsoft.RecoveryServices/vaults/backupPolicies", + "apiVersion": "2022-03-01", + "name": "[format('{0}/{1}', parameters('recoveryServicesVaultName'), 'AvdPolicyStorage')]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.RecoveryServices/vaults'), parameters('tags')['Microsoft.RecoveryServices/vaults'], createObject())]", + "properties": { + "backupManagementType": "AzureStorage", + "schedulePolicy": { + "scheduleRunFrequency": "Daily", + "scheduleRunTimes": [ + "23:00" + ], + "schedulePolicyType": "SimpleSchedulePolicy" + }, + "retentionPolicy": { + "retentionPolicyType": "LongTermRetentionPolicy", + "dailySchedule": { + "retentionTimes": [ + "23:00" + ], + "retentionDuration": { + "count": 30, + "durationType": "Days" + } + } + }, + "timeZone": "[parameters('timeZone')]", + "workLoadType": "AzureFileShare" + }, + "dependsOn": [ + "[resourceId('Microsoft.RecoveryServices/vaults', parameters('recoveryServicesVaultName'))]" + ] + }, + { + "condition": "[not(parameters('fslogix'))]", + "type": "Microsoft.RecoveryServices/vaults/backupPolicies", + "apiVersion": "2022-03-01", + "name": "[format('{0}/{1}', parameters('recoveryServicesVaultName'), 'AvdPolicyVm')]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.RecoveryServices/vaults'), parameters('tags')['Microsoft.RecoveryServices/vaults'], createObject())]", + "properties": { + "backupManagementType": "AzureIaasVM", + "instantRpRetentionRangeInDays": 2, + "policyType": "V2", + "retentionPolicy": { + "retentionPolicyType": "LongTermRetentionPolicy", + "dailySchedule": { + "retentionTimes": [ + "23:00" + ], + "retentionDuration": { + "count": 30, + "durationType": "Days" + } + } + }, + "schedulePolicy": { + "schedulePolicyType": "SimpleSchedulePolicyV2", + "scheduleRunFrequency": "Daily", + "dailySchedule": { + "scheduleRunTimes": [ + "23:00" + ] + } + }, + "timeZone": "[parameters('timeZone')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.RecoveryServices/vaults', parameters('recoveryServicesVaultName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-04-01", + "name": "[format('pe-{0}', parameters('recoveryServicesVaultName'))]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.Network/privateEndpoints'), parameters('tags')['Microsoft.Network/privateEndpoints'], createObject())]", + "properties": { + "customNetworkInterfaceName": "[format('nic-{0}', parameters('recoveryServicesVaultName'))]", + "privateLinkServiceConnections": [ + { + "name": "[format('pe-{0}', parameters('recoveryServicesVaultName'))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.RecoveryServices/vaults', parameters('recoveryServicesVaultName'))]", + "groupIds": [ + "AzureBackup" + ] + } + } + ], + "subnet": { + "id": "[parameters('subnetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.RecoveryServices/vaults', parameters('recoveryServicesVaultName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-08-01", + "name": "[format('{0}/{1}', format('pe-{0}', parameters('recoveryServicesVaultName')), parameters('recoveryServicesVaultName'))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[replace(parameters('recoveryServicesPrivateDnsZoneResourceId'), '.', '-')]", + "properties": { + "privateDnsZoneId": "[parameters('recoveryServicesPrivateDnsZoneResourceId')]" + } + }, + { + "name": "[replace(parameters('azureQueueStoragePrivateDnsZoneResourceId'), '.', '-')]", + "properties": { + "privateDnsZoneId": "[parameters('azureQueueStoragePrivateDnsZoneResourceId')]" + } + }, + { + "name": "[replace(parameters('azureBlobsPrivateDnsZoneResourceId'), '.', '-')]", + "properties": { + "privateDnsZoneId": "[parameters('azureBlobsPrivateDnsZoneResourceId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', format('pe-{0}', parameters('recoveryServicesVaultName')))]", + "[resourceId('Microsoft.RecoveryServices/vaults', parameters('recoveryServicesVaultName'))]" + ] + } + ] + } + } + } + ], + "outputs": { + "artifactsUserAssignedIdentityClientId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(parameters('artifactsStorageAccountResourceId'), '/')[2], split(parameters('artifactsStorageAccountResourceId'), '/')[4]), 'Microsoft.Resources/deployments', format('Artifacts_{0}', parameters('timestamp'))), '2022-09-01').outputs.userAssignedIdentityClientId.value]" + }, + "artifactsUserAssignedIdentityPrincipalId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(parameters('artifactsStorageAccountResourceId'), '/')[2], split(parameters('artifactsStorageAccountResourceId'), '/')[4]), 'Microsoft.Resources/deployments', format('Artifacts_{0}', parameters('timestamp'))), '2022-09-01').outputs.userAssignedIdentityPrincipalId.value]" + }, + "artifactsUserAssignedIdentityResourceId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(parameters('artifactsStorageAccountResourceId'), '/')[2], split(parameters('artifactsStorageAccountResourceId'), '/')[4]), 'Microsoft.Resources/deployments', format('Artifacts_{0}', parameters('timestamp'))), '2022-09-01').outputs.userAssignedIdentityResourceId.value]" + }, + "dataCollectionRuleResourceId": { + "type": "string", + "value": "[if(parameters('enableMonitoring'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('Monitoring_{0}', parameters('timestamp'))), '2022-09-01').outputs.dataCollectionRuleResourceId.value, '')]" + }, + "deploymentUserAssignedIdentityClientId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp'))), '2022-09-01').outputs.clientId.value]" + }, + "deploymentUserAssignedIdentityPrincipalId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp'))), '2022-09-01').outputs.principalId.value]" + }, + "deploymentUserAssignedIdentityResourceId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('UserAssignedIdentity_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceId.value]" + }, + "diskEncryptionSetResourceId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('DiskEncryptionSet_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceId.value]" + }, + "encryptionUserAssignedIdentityClientId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('CustomerManagedKeys_{0}', parameters('timestamp'))), '2022-09-01').outputs.encryptionUserAssignedIdentityClientId.value]" + }, + "encryptionUserAssignedIdentityPrincipalId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('CustomerManagedKeys_{0}', parameters('timestamp'))), '2022-09-01').outputs.encryptionUserAssignedIdentityPrincipalId.value]" + }, + "encryptionUserAssignedIdentityResourceId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('CustomerManagedKeys_{0}', parameters('timestamp'))), '2022-09-01').outputs.encryptionUserAssignedIdentityResourceId.value]" + }, + "existingFeedWorkspace": { + "type": "bool", + "value": "[if(equals(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('Validations_{0}', parameters('timestamp'))), '2022-09-01').outputs.value.value.existingWorkspace, 'true'), true(), false())]" + }, + "hybridRunbookWorkerGroupName": { + "type": "string", + "value": "[if(or(parameters('scalingTool'), equals(parameters('fslogixStorageService'), 'AzureFiles Premium')), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('AutomationAccount_{0}', parameters('timestamp'))), '2022-09-01').outputs.hybridRunbookWorkerGroupName.value, '')]" + }, + "keyVaultUri": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('CustomerManagedKeys_{0}', parameters('timestamp'))), '2022-09-01').outputs.keyVaultUri.value]" + }, + "logAnalyticsWorkspaceResourceId": { + "type": "string", + "value": "[if(parameters('enableMonitoring'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('Monitoring_{0}', parameters('timestamp'))), '2022-09-01').outputs.logAnalyticsWorkspaceResourceId.value, '')]" + }, + "storageEncryptionKeyName": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('CustomerManagedKeys_{0}', parameters('timestamp'))), '2022-09-01').outputs.storageKeyName.value]" + }, + "validateAcceleratedNetworking": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('Validations_{0}', parameters('timestamp'))), '2022-09-01').outputs.value.value.acceleratedNetworking]" + }, + "validateANFDnsServers": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('Validations_{0}', parameters('timestamp'))), '2022-09-01').outputs.value.value.anfDnsServers]" + }, + "validateANFfActiveDirectory": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('Validations_{0}', parameters('timestamp'))), '2022-09-01').outputs.value.value.anfActiveDirectory]" + }, + "validateANFSubnetId": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('Validations_{0}', parameters('timestamp'))), '2022-09-01').outputs.value.value.anfSubnetId]" + }, + "validateAvailabilityZones": { + "type": "array", + "value": "[if(equals(parameters('availability'), 'AvailabilityZones'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('Validations_{0}', parameters('timestamp'))), '2022-09-01').outputs.value.value.availabilityZones, createArray('1'))]" + }, + "virtualMachineName": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('ManagementVirtualMachine_{0}', parameters('timestamp'))), '2022-09-01').outputs.Name.value]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Network_Hosts_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp')))]", + "rgs" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Hub_{0}', parameters('timestamp'))]", + "subscriptionId": "[split(parameters('hubSubnetResourceId'), '/')[2]]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "existingWorkspace": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.existingFeedWorkspace.value]" + }, + "globalWorkspacePrivateDnsZoneResourceId": { + "value": "[format('{0}{1}', variables('privateDnsZoneResourceIdPrefix'), reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.avdGlobalPrivateDnsZoneName.value)]" + }, + "hubSubnetResourceId": { + "value": "[parameters('hubSubnetResourceId')]" + }, + "resourceGroupName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupGlobalWorkspace.value]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "workspaceNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.workspaceGlobalNamePrefix.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12762424075807203193" + } + }, + "parameters": { + "existingWorkspace": { + "type": "bool" + }, + "globalWorkspacePrivateDnsZoneResourceId": { + "type": "string" + }, + "hubSubnetResourceId": { + "type": "string" + }, + "resourceGroupName": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "workspaceNamePrefix": { + "type": "string" + } + }, + "resources": [ + { + "condition": "[not(parameters('existingWorkspace'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('SharedServices_VirtualNetwork_{0}', parameters('timestamp'))]", + "resourceGroup": "[split(parameters('hubSubnetResourceId'), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[split(parameters('hubSubnetResourceId'), '/')[8]]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "4601561707996553843" + } + }, + "parameters": { + "name": { + "type": "string" + } + }, + "resources": [], + "outputs": { + "location": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Network/virtualNetworks', parameters('name')), '2023-06-01', 'full').location]" + } + } + } + } + }, + { + "condition": "[not(parameters('existingWorkspace'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('ResourceGroup_WorkspaceGlobal_{0}', parameters('timestamp'))]", + "subscriptionId": "[split(parameters('hubSubnetResourceId'), '/')[2]]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": "[if(not(parameters('existingWorkspace')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, split(parameters('hubSubnetResourceId'), '/')[4]), 'Microsoft.Resources/deployments', format('SharedServices_VirtualNetwork_{0}', parameters('timestamp'))), '2022-09-01').outputs.location.value), createObject('value', ''))]", + "resourceGroupName": { + "value": "[parameters('resourceGroupName')]" + }, + "tags": { + "value": {} + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "938236069915523781" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "resourceGroupName": { + "type": "string" + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2020-10-01", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('location')]", + "tags": "[if(contains(parameters('tags'), 'Microsoft.Resources/resourceGroups'), parameters('tags')['Microsoft.Resources/resourceGroups'], createObject())]" + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, split(parameters('hubSubnetResourceId'), '/')[4]), 'Microsoft.Resources/deployments', format('SharedServices_VirtualNetwork_{0}', parameters('timestamp')))]" + ] + }, + { + "condition": "[not(parameters('existingWorkspace'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('WorkspaceGlobal_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "globalWorkspacePrivateDnsZoneResourceId": { + "value": "[parameters('globalWorkspacePrivateDnsZoneResourceId')]" + }, + "location": "[if(not(parameters('existingWorkspace')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, split(parameters('hubSubnetResourceId'), '/')[4]), 'Microsoft.Resources/deployments', format('SharedServices_VirtualNetwork_{0}', parameters('timestamp'))), '2022-09-01').outputs.location.value), createObject('value', ''))]", + "subnetResourceId": { + "value": "[parameters('hubSubnetResourceId')]" + }, + "workspaceNamePrefix": { + "value": "[parameters('workspaceNamePrefix')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "6673684744673325063" + } + }, + "parameters": { + "globalWorkspacePrivateDnsZoneResourceId": { + "type": "string" + }, + "location": { + "type": "string" + }, + "subnetResourceId": { + "type": "string" + }, + "workspaceNamePrefix": { + "type": "string" + } + }, + "variables": { + "globalWorkspaceName": "[format('{0}-global', parameters('workspaceNamePrefix'))]", + "privateEndpointName": "[format('pe-{0}', variables('globalWorkspaceName'))]" + }, + "resources": [ + { + "type": "Microsoft.DesktopVirtualization/workspaces", + "apiVersion": "2023-09-05", + "name": "[variables('globalWorkspaceName')]", + "location": "[parameters('location')]", + "tags": {}, + "properties": {} + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-04-01", + "name": "[variables('privateEndpointName')]", + "location": "[parameters('location')]", + "tags": {}, + "properties": { + "customNetworkInterfaceName": "[format('nic-{0}', variables('globalWorkspaceName'))]", + "privateLinkServiceConnections": [ + { + "name": "[variables('privateEndpointName')]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.DesktopVirtualization/workspaces', variables('globalWorkspaceName'))]", + "groupIds": [ + "global" + ] + } + } + ], + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DesktopVirtualization/workspaces', variables('globalWorkspaceName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', variables('privateEndpointName'), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[replace(split(parameters('globalWorkspacePrivateDnsZoneResourceId'), '/')[8], '.', '-')]", + "properties": { + "privateDnsZoneId": "[parameters('globalWorkspacePrivateDnsZoneResourceId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('privateEndpointName'))]" + ] + } + ] + } + }, + "dependsOn": [ + "[subscriptionResourceId(split(parameters('hubSubnetResourceId'), '/')[2], 'Microsoft.Resources/deployments', format('ResourceGroup_WorkspaceGlobal_{0}', parameters('timestamp')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, split(parameters('hubSubnetResourceId'), '/')[4]), 'Microsoft.Resources/deployments', format('SharedServices_VirtualNetwork_{0}', parameters('timestamp')))]" + ] + } + ] + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('ControlPlane_{0}', parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "activeDirectorySolution": { + "value": "[parameters('activeDirectorySolution')]" + }, + "artifactsUri": { + "value": "[variables('artifactsUri')]" + }, + "avdPrivateDnsZoneResourceId": { + "value": "[format('{0}{1}', variables('privateDnsZoneResourceIdPrefix'), reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.avdPrivateDnsZoneName.value)]" + }, + "customRdpProperty": { + "value": "[parameters('customRdpProperty')]" + }, + "deploymentUserAssignedIdentityClientId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.deploymentUserAssignedIdentityClientId.value]" + }, + "desktopApplicationGroupName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.desktopApplicationGroupName.value]" + }, + "desktopFriendlyName": { + "value": "[parameters('desktopFriendlyName')]" + }, + "existingFeedWorkspace": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.existingFeedWorkspace.value]" + }, + "hostPoolName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.hostPoolName.value]" + }, + "hostPoolPublicNetworkAccess": { + "value": "[parameters('hostPoolPublicNetworkAccess')]" + }, + "hostPoolType": { + "value": "[parameters('hostPoolType')]" + }, + "locationControlPlane": { + "value": "[parameters('locationControlPlane')]" + }, + "locationVirtualMachines": { + "value": "[parameters('locationVirtualMachines')]" + }, + "logAnalyticsWorkspaceResourceId": "[if(parameters('monitoring'), createObject('value', reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.logAnalyticsWorkspaceResourceId.value), createObject('value', ''))]", + "managementVirtualMachineName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.virtualMachineName.value]" + }, + "maxSessionLimit": { + "value": "[parameters('maxSessionLimit')]" + }, + "monitoring": { + "value": "[parameters('monitoring')]" + }, + "resourceGroupControlPlane": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupControlPlane.value]" + }, + "resourceGroupFeedWorkspace": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupFeedWorkspace.value]" + }, + "resourceGroupManagement": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupManagement.value]" + }, + "roleDefinitions": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.roleDefinitions.value]" + }, + "securityPrincipalObjectIds": { + "value": "[map(parameters('securityPrincipals'), lambda('item', lambdaVariables('item').objectId))]" + }, + "subnetResourceId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "validationEnvironment": { + "value": "[parameters('validationEnvironment')]" + }, + "vmTemplate": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.vmTemplate.value]" + }, + "workspaceFriendlyName": { + "value": "[parameters('workspaceFriendlyName')]" + }, + "workspaceNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.workspaceFeedNamePrefix.value]" + }, + "workspacePublicNetworkAccess": { + "value": "[parameters('workspacePublicNetworkAccess')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "6802657512862564486" + } + }, + "parameters": { + "activeDirectorySolution": { + "type": "string" + }, + "artifactsUri": { + "type": "string" + }, + "avdPrivateDnsZoneResourceId": { + "type": "string" + }, + "customRdpProperty": { + "type": "string" + }, + "deploymentUserAssignedIdentityClientId": { + "type": "string" + }, + "desktopApplicationGroupName": { + "type": "string" + }, + "desktopFriendlyName": { + "type": "string" + }, + "existingFeedWorkspace": { + "type": "bool" + }, + "hostPoolName": { + "type": "string" + }, + "hostPoolPublicNetworkAccess": { + "type": "string" + }, + "hostPoolType": { + "type": "string" + }, + "locationControlPlane": { + "type": "string" + }, + "locationVirtualMachines": { + "type": "string" + }, + "logAnalyticsWorkspaceResourceId": { + "type": "string" + }, + "managementVirtualMachineName": { + "type": "string" + }, + "maxSessionLimit": { + "type": "int" + }, + "monitoring": { + "type": "bool" + }, + "resourceGroupControlPlane": { + "type": "string" + }, + "resourceGroupFeedWorkspace": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "roleDefinitions": { + "type": "object" + }, + "securityPrincipalObjectIds": { + "type": "array" + }, + "subnetResourceId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "validationEnvironment": { + "type": "bool" + }, + "vmTemplate": { + "type": "string" + }, + "workspaceFriendlyName": { + "type": "string" + }, + "workspaceNamePrefix": { + "type": "string" + }, + "workspacePublicNetworkAccess": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('HostPool_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupControlPlane')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "activeDirectorySolution": { + "value": "[parameters('activeDirectorySolution')]" + }, + "avdPrivateDnsZoneResourceId": { + "value": "[parameters('avdPrivateDnsZoneResourceId')]" + }, + "customRdpProperty": { + "value": "[parameters('customRdpProperty')]" + }, + "hostPoolName": { + "value": "[parameters('hostPoolName')]" + }, + "hostPoolPublicNetworkAccess": { + "value": "[parameters('hostPoolPublicNetworkAccess')]" + }, + "hostPoolType": { + "value": "[parameters('hostPoolType')]" + }, + "location": { + "value": "[parameters('locationControlPlane')]" + }, + "logAnalyticsWorkspaceResourceId": { + "value": "[parameters('logAnalyticsWorkspaceResourceId')]" + }, + "maxSessionLimit": { + "value": "[parameters('maxSessionLimit')]" + }, + "monitoring": { + "value": "[parameters('monitoring')]" + }, + "subnetResourceId": { + "value": "[parameters('subnetResourceId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "validationEnvironment": { + "value": "[parameters('validationEnvironment')]" + }, + "vmTemplate": { + "value": "[parameters('vmTemplate')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12762592848284809417" + } + }, + "parameters": { + "activeDirectorySolution": { + "type": "string" + }, + "avdPrivateDnsZoneResourceId": { + "type": "string" + }, + "customRdpProperty": { + "type": "string" + }, + "hostPoolName": { + "type": "string" + }, + "hostPoolPublicNetworkAccess": { + "type": "string" + }, + "hostPoolType": { + "type": "string" + }, + "location": { + "type": "string" + }, + "logAnalyticsWorkspaceResourceId": { + "type": "string" + }, + "maxSessionLimit": { + "type": "int" + }, + "monitoring": { + "type": "bool" + }, + "subnetResourceId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "time": { + "type": "string", + "defaultValue": "[utcNow('u')]" + }, + "validationEnvironment": { + "type": "bool" + }, + "vmTemplate": { + "type": "string" + } + }, + "variables": { + "customRdpProperty_Complete": "[if(contains(parameters('activeDirectorySolution'), 'MicrosoftEntraId'), format('{0}targetisaadjoined:i:1;enablerdsaadauth:i:1;', parameters('customRdpProperty')), parameters('customRdpProperty'))]", + "hostPoolLogs": [ + { + "category": "Checkpoint", + "enabled": true + }, + { + "category": "Error", + "enabled": true + }, + { + "category": "Management", + "enabled": true + }, + { + "category": "Connection", + "enabled": true + }, + { + "category": "HostRegistration", + "enabled": true + }, + { + "category": "AgentHealthStatus", + "enabled": true + } + ], + "privateEndpointName": "[format('pe-{0}', parameters('hostPoolName'))]" + }, + "resources": [ + { + "type": "Microsoft.DesktopVirtualization/hostPools", + "apiVersion": "2023-09-05", + "name": "[parameters('hostPoolName')]", + "location": "[parameters('location')]", + "tags": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, resourceGroup().name, parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.DesktopVirtualization/hostPools'), parameters('tags')['Microsoft.DesktopVirtualization/hostPools'], createObject()))]", + "properties": { + "customRdpProperty": "[variables('customRdpProperty_Complete')]", + "hostPoolType": "[split(parameters('hostPoolType'), ' ')[0]]", + "loadBalancerType": "[if(contains(parameters('hostPoolType'), 'Pooled'), split(parameters('hostPoolType'), ' ')[1], 'Persistent')]", + "maxSessionLimit": "[parameters('maxSessionLimit')]", + "personalDesktopAssignmentType": "[if(contains(parameters('hostPoolType'), 'Personal'), split(parameters('hostPoolType'), ' ')[1], null())]", + "preferredAppGroupType": "Desktop", + "publicNetworkAccess": "[parameters('hostPoolPublicNetworkAccess')]", + "registrationInfo": { + "expirationTime": "[dateTimeAdd(parameters('time'), 'PT2H')]", + "registrationTokenOperation": "Update" + }, + "startVMOnConnect": true, + "validationEnvironment": "[parameters('validationEnvironment')]", + "vmTemplate": "[parameters('vmTemplate')]" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-04-01", + "name": "[variables('privateEndpointName')]", + "location": "[parameters('location')]", + "tags": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, resourceGroup().name, parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Network/privateEndpoints'), parameters('tags')['Microsoft.Network/privateEndpoints'], createObject()))]", + "properties": { + "customNetworkInterfaceName": "[format('nic-{0}', parameters('hostPoolName'))]", + "privateLinkServiceConnections": [ + { + "name": "[variables('privateEndpointName')]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.DesktopVirtualization/hostPools', parameters('hostPoolName'))]", + "groupIds": [ + "connection" + ] + } + } + ], + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DesktopVirtualization/hostPools', parameters('hostPoolName'))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', variables('privateEndpointName'), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[replace(split(parameters('avdPrivateDnsZoneResourceId'), '/')[8], '.', '-')]", + "properties": { + "privateDnsZoneId": "[parameters('avdPrivateDnsZoneResourceId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('privateEndpointName'))]" + ] + }, + { + "condition": "[parameters('monitoring')]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.DesktopVirtualization/hostPools/{0}', parameters('hostPoolName'))]", + "name": "[format('diag-{0}', parameters('hostPoolName'))]", + "properties": { + "logs": "[variables('hostPoolLogs')]", + "workspaceId": "[parameters('logAnalyticsWorkspaceResourceId')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.DesktopVirtualization/hostPools', parameters('hostPoolName'))]" + ] + } + ], + "outputs": { + "ResourceId": { + "type": "string", + "value": "[resourceId('Microsoft.DesktopVirtualization/hostPools', parameters('hostPoolName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('ApplicationGroup_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupControlPlane')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "artifactsUri": { + "value": "[parameters('artifactsUri')]" + }, + "deploymentUserAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + }, + "desktopApplicationGroupName": { + "value": "[parameters('desktopApplicationGroupName')]" + }, + "hostPoolResourceId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupControlPlane')), 'Microsoft.Resources/deployments', format('HostPool_{0}', parameters('timestamp'))), '2022-09-01').outputs.ResourceId.value]" + }, + "locationControlPlane": { + "value": "[parameters('locationControlPlane')]" + }, + "locationVirtualMachines": { + "value": "[parameters('locationVirtualMachines')]" + }, + "resourceGroupManagement": { + "value": "[parameters('resourceGroupManagement')]" + }, + "roleDefinitions": { + "value": "[parameters('roleDefinitions')]" + }, + "securityPrincipalObjectIds": { + "value": "[parameters('securityPrincipalObjectIds')]" + }, + "desktopFriendlyName": { + "value": "[parameters('desktopFriendlyName')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "virtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "8892505472052746814" + } + }, + "parameters": { + "artifactsUri": { + "type": "string" + }, + "deploymentUserAssignedIdentityClientId": { + "type": "string" + }, + "desktopApplicationGroupName": { + "type": "string" + }, + "desktopFriendlyName": { + "type": "string" + }, + "hostPoolResourceId": { + "type": "string" + }, + "locationControlPlane": { + "type": "string" + }, + "locationVirtualMachines": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "roleDefinitions": { + "type": "object" + }, + "securityPrincipalObjectIds": { + "type": "array" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.DesktopVirtualization/applicationGroups", + "apiVersion": "2021-03-09-preview", + "name": "[parameters('desktopApplicationGroupName')]", + "location": "[parameters('locationControlPlane')]", + "tags": "[union(createObject('cm-resource-parent', parameters('hostPoolResourceId')), if(contains(parameters('tags'), 'Microsoft.DesktopVirtualization/applicationGroups'), parameters('tags')['Microsoft.DesktopVirtualization/applicationGroups'], createObject()))]", + "properties": { + "hostPoolArmPath": "[parameters('hostPoolResourceId')]", + "applicationGroupType": "Desktop" + } + }, + { + "copy": { + "name": "roleAssignment", + "count": "[length(range(0, length(parameters('securityPrincipalObjectIds'))))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.DesktopVirtualization/applicationGroups/{0}', parameters('desktopApplicationGroupName'))]", + "name": "[guid(parameters('securityPrincipalObjectIds')[range(0, length(parameters('securityPrincipalObjectIds')))[copyIndex()]], parameters('roleDefinitions').DesktopVirtualizationUser, parameters('desktopApplicationGroupName'))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitions').DesktopVirtualizationUser)]", + "principalId": "[parameters('securityPrincipalObjectIds')[range(0, length(parameters('securityPrincipalObjectIds')))[copyIndex()]]]" + }, + "dependsOn": [ + "[resourceId('Microsoft.DesktopVirtualization/applicationGroups', parameters('desktopApplicationGroupName'))]" + ] + }, + { + "condition": "[not(empty(parameters('desktopFriendlyName')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('ApplicationFriendlyName_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileUris": { + "value": [ + "[format('{0}Update-AvdDesktop.ps1', parameters('artifactsUri'))]" + ] + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "parameters": { + "value": "[format('-ApplicationGroupName {0} -Environment {1} -FriendlyName \"{2}\" -ResourceGroupName {3} -SubscriptionId {4} -Tenant {5} -UserAssignedIdentityClientId {6}', parameters('desktopApplicationGroupName'), environment().name, parameters('desktopFriendlyName'), resourceGroup().name, subscription().subscriptionId, tenant().tenantId, parameters('deploymentUserAssignedIdentityClientId'))]" + }, + "scriptFileName": { + "value": "Update-AvdDesktop.ps1" + }, + "tags": { + "value": "[union(createObject('cm-resource-parent', parameters('hostPoolResourceId')), if(contains(parameters('tags'), 'Microsoft.Compute/virtualMachines'), parameters('tags')['Microsoft.Compute/virtualMachines'], createObject()))]" + }, + "userAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + }, + "virtualMachineName": { + "value": "[parameters('virtualMachineName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12496195847910288876" + } + }, + "parameters": { + "fileUris": { + "type": "array" + }, + "location": { + "type": "string" + }, + "parameters": { + "type": "securestring" + }, + "scriptFileName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]" + }, + "userAssignedIdentityClientId": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), 'CustomScriptExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "timestamp": "[parameters('timestamp')]" + }, + "protectedSettings": { + "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File {0} {1}', parameters('scriptFileName'), parameters('parameters'))]", + "fileUris": "[parameters('fileUris')]", + "managedIdentity": { + "clientId": "[parameters('userAssignedIdentityClientId')]" + } + } + } + } + ], + "outputs": { + "value": { + "type": "object", + "value": "[json(filter(reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'CustomScriptExtension'), '2021-03-01').instanceView.substatuses, lambda('item', equals(lambdaVariables('item').code, 'ComponentStatus/StdOut/succeeded')))[0].message)]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DesktopVirtualization/applicationGroups', parameters('desktopApplicationGroupName'))]" + ] + } + ], + "outputs": { + "applicationGroupReference": { + "type": "array", + "value": [ + "[resourceId('Microsoft.DesktopVirtualization/applicationGroups', parameters('desktopApplicationGroupName'))]" + ] + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.DesktopVirtualization/applicationGroups', parameters('desktopApplicationGroupName'))]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupControlPlane')), 'Microsoft.Resources/deployments', format('HostPool_{0}', parameters('timestamp')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('WorkspaceFeed_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupFeedWorkspace')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "applicationGroupReferences": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupControlPlane')), 'Microsoft.Resources/deployments', format('ApplicationGroup_{0}', parameters('timestamp'))), '2022-09-01').outputs.applicationGroupReference.value]" + }, + "artifactsUri": { + "value": "[parameters('artifactsUri')]" + }, + "avdPrivateDnsZoneResourceId": { + "value": "[parameters('avdPrivateDnsZoneResourceId')]" + }, + "deploymentUserAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + }, + "existing": { + "value": "[parameters('existingFeedWorkspace')]" + }, + "friendlyName": { + "value": "[parameters('workspaceFriendlyName')]" + }, + "hostPoolName": { + "value": "[parameters('hostPoolName')]" + }, + "locationControlPlane": { + "value": "[parameters('locationControlPlane')]" + }, + "locationVirtualMachines": { + "value": "[parameters('locationVirtualMachines')]" + }, + "logAnalyticsWorkspaceResourceId": { + "value": "[parameters('logAnalyticsWorkspaceResourceId')]" + }, + "monitoring": { + "value": "[parameters('monitoring')]" + }, + "resourceGroupManagement": { + "value": "[parameters('resourceGroupManagement')]" + }, + "subnetResourceId": { + "value": "[parameters('subnetResourceId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "virtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + }, + "workspaceNamePrefix": { + "value": "[parameters('workspaceNamePrefix')]" + }, + "workspacePublicNetworkAccess": { + "value": "[parameters('workspacePublicNetworkAccess')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "5126346577469036138" + } + }, + "parameters": { + "applicationGroupReferences": { + "type": "array" + }, + "artifactsUri": { + "type": "string" + }, + "avdPrivateDnsZoneResourceId": { + "type": "string" + }, + "deploymentUserAssignedIdentityClientId": { + "type": "string" + }, + "existing": { + "type": "bool" + }, + "friendlyName": { + "type": "string" + }, + "hostPoolName": { + "type": "string" + }, + "locationControlPlane": { + "type": "string" + }, + "locationVirtualMachines": { + "type": "string" + }, + "logAnalyticsWorkspaceResourceId": { + "type": "string" + }, + "monitoring": { + "type": "bool" + }, + "resourceGroupManagement": { + "type": "string" + }, + "subnetResourceId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + }, + "workspaceNamePrefix": { + "type": "string" + }, + "workspacePublicNetworkAccess": { + "type": "string" + } + }, + "variables": { + "feedWorkspaceName": "[format('{0}-feed', parameters('workspaceNamePrefix'))]", + "privateEndpointName": "[format('pe-{0}', variables('feedWorkspaceName'))]" + }, + "resources": [ + { + "condition": "[not(parameters('existing'))]", + "type": "Microsoft.DesktopVirtualization/workspaces", + "apiVersion": "2023-09-05", + "name": "[variables('feedWorkspaceName')]", + "location": "[parameters('locationControlPlane')]", + "tags": {}, + "properties": { + "applicationGroupReferences": "[parameters('applicationGroupReferences')]", + "friendlyName": "[format('{0} ({1})', parameters('friendlyName'), parameters('locationControlPlane'))]", + "publicNetworkAccess": "[parameters('workspacePublicNetworkAccess')]" + } + }, + { + "condition": "[not(parameters('existing'))]", + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-04-01", + "name": "[variables('privateEndpointName')]", + "location": "[parameters('locationControlPlane')]", + "tags": {}, + "properties": { + "customNetworkInterfaceName": "[format('nic-{0}', variables('feedWorkspaceName'))]", + "privateLinkServiceConnections": [ + { + "name": "[variables('privateEndpointName')]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.DesktopVirtualization/workspaces', variables('feedWorkspaceName'))]", + "groupIds": [ + "feed" + ] + } + } + ], + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DesktopVirtualization/workspaces', variables('feedWorkspaceName'))]" + ] + }, + { + "condition": "[not(parameters('existing'))]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', variables('privateEndpointName'), 'default')]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "[replace(split(parameters('avdPrivateDnsZoneResourceId'), '/')[8], '.', '-')]", + "properties": { + "privateDnsZoneId": "[parameters('avdPrivateDnsZoneResourceId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('privateEndpointName'))]" + ] + }, + { + "condition": "[and(not(parameters('existing')), parameters('monitoring'))]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.DesktopVirtualization/workspaces/{0}', variables('feedWorkspaceName'))]", + "name": "[format('diag-{0}', variables('feedWorkspaceName'))]", + "properties": { + "logs": [ + { + "category": "Checkpoint", + "enabled": true + }, + { + "category": "Error", + "enabled": true + }, + { + "category": "Management", + "enabled": true + }, + { + "category": "Feed", + "enabled": true + } + ], + "workspaceId": "[parameters('logAnalyticsWorkspaceResourceId')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.DesktopVirtualization/workspaces', variables('feedWorkspaceName'))]" + ] + }, + { + "condition": "[parameters('existing')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('AddApplicationGroupReferences_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileUris": { + "value": [ + "[format('{0}Update-AvdWorkspace.ps1', parameters('artifactsUri'))]" + ] + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "parameters": { + "value": "[format('-ApplicationGroupReferences \"{0}\" -Environment {1} -ResourceGroupName {2} -SubscriptionId {3} -TenantId {4} -UserAssignedIdentityClientId {5} -WorkspaceName {6}', parameters('applicationGroupReferences'), environment().name, resourceGroup().name, subscription().subscriptionId, tenant().tenantId, parameters('deploymentUserAssignedIdentityClientId'), variables('feedWorkspaceName'))]" + }, + "scriptFileName": { + "value": "Update-AvdWorkspace.ps1" + }, + "tags": { + "value": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, resourceGroup().name, parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Compute/virtualMachines'), parameters('tags')['Microsoft.Compute/virtualMachines'], createObject()))]" + }, + "userAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + }, + "virtualMachineName": { + "value": "[parameters('virtualMachineName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12496195847910288876" + } + }, + "parameters": { + "fileUris": { + "type": "array" + }, + "location": { + "type": "string" + }, + "parameters": { + "type": "securestring" + }, + "scriptFileName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]" + }, + "userAssignedIdentityClientId": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), 'CustomScriptExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "timestamp": "[parameters('timestamp')]" + }, + "protectedSettings": { + "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File {0} {1}', parameters('scriptFileName'), parameters('parameters'))]", + "fileUris": "[parameters('fileUris')]", + "managedIdentity": { + "clientId": "[parameters('userAssignedIdentityClientId')]" + } + } + } + } + ], + "outputs": { + "value": { + "type": "object", + "value": "[json(filter(reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'CustomScriptExtension'), '2021-03-01').instanceView.substatuses, lambda('item', equals(lambdaVariables('item').code, 'ComponentStatus/StdOut/succeeded')))[0].message)]" + } + } + } + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupControlPlane')), 'Microsoft.Resources/deployments', format('ApplicationGroup_{0}', parameters('timestamp')))]" + ] + } + ] + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp')))]", + "rgs" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('FSLogix_{0}', parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "activeDirectoryConnection": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.validateANFfActiveDirectory.value]" + }, + "activeDirectorySolution": { + "value": "[parameters('activeDirectorySolution')]" + }, + "artifactsUri": { + "value": "[variables('artifactsUri')]" + }, + "automationAccountName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.automationAccountName.value]" + }, + "availability": { + "value": "[parameters('availability')]" + }, + "azureFilesPrivateDnsZoneResourceId": { + "value": "[format('{0}{1}', variables('privateDnsZoneResourceIdPrefix'), reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.filePrivateDnsZoneName.value)]" + }, + "delegatedSubnetId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.validateANFSubnetId.value]" + }, + "deploymentUserAssignedIdentityClientId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.deploymentUserAssignedIdentityClientId.value]" + }, + "dnsServers": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.validateANFDnsServers.value]" + }, + "domainJoinPassword": { + "value": "[parameters('domainJoinPassword')]" + }, + "domainJoinUserPrincipalName": { + "value": "[parameters('domainJoinUserPrincipalName')]" + }, + "domainName": { + "value": "[parameters('domainName')]" + }, + "encryptionUserAssignedIdentityResourceId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.encryptionUserAssignedIdentityResourceId.value]" + }, + "fileShares": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.fileShares.value]" + }, + "fslogixContainerType": { + "value": "[parameters('fslogixContainerType')]" + }, + "fslogixShareSizeInGB": { + "value": "[parameters('fslogixShareSizeInGB')]" + }, + "fslogixStorageService": { + "value": "[parameters('fslogixStorageService')]" + }, + "hostPoolName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.hostPoolName.value]" + }, + "hostPoolType": { + "value": "[parameters('hostPoolType')]" + }, + "keyVaultUri": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.keyVaultUri.value]" + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "managementVirtualMachineName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.virtualMachineName.value]" + }, + "netAppAccountName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.netAppAccountName.value]" + }, + "netAppCapacityPoolName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.netAppCapacityPoolName.value]" + }, + "netbios": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.netbios.value]" + }, + "organizationalUnitPath": { + "value": "[parameters('organizationalUnitPath')]" + }, + "recoveryServices": { + "value": "[parameters('recoveryServices')]" + }, + "recoveryServicesVaultName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.recoveryServicesVaultName.value]" + }, + "resourceGroupControlPlane": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupControlPlane.value]" + }, + "resourceGroupManagement": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupManagement.value]" + }, + "resourceGroupStorage": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupStorage.value]" + }, + "securityPrincipalNames": { + "value": "[map(parameters('securityPrincipals'), lambda('item', lambdaVariables('item').name))]" + }, + "securityPrincipalObjectIds": { + "value": "[map(parameters('securityPrincipals'), lambda('item', lambdaVariables('item').objectId))]" + }, + "smbServerLocation": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.smbServerLocation.value]" + }, + "storageAccountNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.storageAccountNamePrefix.value]" + }, + "storageCount": { + "value": "[parameters('storageCount')]" + }, + "storageEncryptionKeyName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.storageEncryptionKeyName.value]" + }, + "storageIndex": { + "value": "[parameters('storageIndex')]" + }, + "storageService": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.storageService.value]" + }, + "storageSku": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.storageSku.value]" + }, + "subnet": "[if(equals(length(variables('deploymentLocations')), 1), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[10]), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_Hosts_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[10]))]", + "tags": { + "value": "[parameters('tags')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "timeZone": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.timeZone.value]" + }, + "virtualNetwork": "[if(equals(length(variables('deploymentLocations')), 1), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[8]), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_Hosts_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[8]))]", + "virtualNetworkResourceGroup": "[if(equals(length(variables('deploymentLocations')), 1), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[4]), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_Hosts_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[4]))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "5718452863906302578" + } + }, + "parameters": { + "artifactsUri": { + "type": "string" + }, + "activeDirectoryConnection": { + "type": "string" + }, + "activeDirectorySolution": { + "type": "string" + }, + "automationAccountName": { + "type": "string" + }, + "availability": { + "type": "string" + }, + "azureFilesPrivateDnsZoneResourceId": { + "type": "string" + }, + "delegatedSubnetId": { + "type": "string" + }, + "deploymentUserAssignedIdentityClientId": { + "type": "string" + }, + "dnsServers": { + "type": "string" + }, + "domainJoinPassword": { + "type": "securestring" + }, + "domainJoinUserPrincipalName": { + "type": "string" + }, + "domainName": { + "type": "string" + }, + "encryptionUserAssignedIdentityResourceId": { + "type": "string" + }, + "fileShares": { + "type": "array" + }, + "fslogixShareSizeInGB": { + "type": "int" + }, + "fslogixContainerType": { + "type": "string" + }, + "fslogixStorageService": { + "type": "string" + }, + "hostPoolName": { + "type": "string" + }, + "hostPoolType": { + "type": "string" + }, + "keyVaultUri": { + "type": "string" + }, + "location": { + "type": "string" + }, + "managementVirtualMachineName": { + "type": "string" + }, + "netAppAccountName": { + "type": "string" + }, + "netAppCapacityPoolName": { + "type": "string" + }, + "netbios": { + "type": "string" + }, + "organizationalUnitPath": { + "type": "string" + }, + "recoveryServices": { + "type": "bool" + }, + "recoveryServicesVaultName": { + "type": "string" + }, + "resourceGroupControlPlane": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "resourceGroupStorage": { + "type": "string" + }, + "securityPrincipalObjectIds": { + "type": "array" + }, + "securityPrincipalNames": { + "type": "array" + }, + "smbServerLocation": { + "type": "string" + }, + "storageAccountNamePrefix": { + "type": "string" + }, + "storageCount": { + "type": "int" + }, + "storageEncryptionKeyName": { + "type": "string" + }, + "storageIndex": { + "type": "int" + }, + "storageSku": { + "type": "string" + }, + "storageService": { + "type": "string" + }, + "subnet": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "virtualNetwork": { + "type": "string" + }, + "virtualNetworkResourceGroup": { + "type": "string" + } + }, + "variables": { + "tagsAutomationAccounts": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupControlPlane'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Automation/automationAccounts'), parameters('tags')['Microsoft.Automation/automationAccounts'], createObject()))]", + "tagsNetAppAccount": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupControlPlane'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.NetApp/netAppAccounts'), parameters('tags')['Microsoft.NetApp/netAppAccounts'], createObject()))]", + "tagsPrivateEndpoints": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupControlPlane'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Network/privateEndpoints'), parameters('tags')['Microsoft.Network/privateEndpoints'], createObject()))]", + "tagsStorageAccounts": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupControlPlane'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Storage/storageAccounts'), parameters('tags')['Microsoft.Storage/storageAccounts'], createObject()))]", + "tagsRecoveryServicesVault": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupControlPlane'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.recoveryServices/vaults'), parameters('tags')['Microsoft.recoveryServices/vaults'], createObject()))]", + "tagsVirtualMachines": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupControlPlane'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Compute/virtualMachines'), parameters('tags')['Microsoft.Compute/virtualMachines'], createObject()))]" + }, + "resources": [ + { + "condition": "[and(equals(parameters('storageService'), 'AzureNetAppFiles'), contains(parameters('activeDirectorySolution'), 'DomainServices'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('AzureNetAppFiles_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupStorage')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "artifactsUri": { + "value": "[parameters('artifactsUri')]" + }, + "activeDirectoryConnection": { + "value": "[parameters('activeDirectoryConnection')]" + }, + "delegatedSubnetId": { + "value": "[parameters('delegatedSubnetId')]" + }, + "dnsServers": { + "value": "[parameters('dnsServers')]" + }, + "domainJoinPassword": { + "value": "[parameters('domainJoinPassword')]" + }, + "domainJoinUserPrincipalName": { + "value": "[parameters('domainJoinUserPrincipalName')]" + }, + "domainName": { + "value": "[parameters('domainName')]" + }, + "fileShares": { + "value": "[parameters('fileShares')]" + }, + "fslogixContainerType": { + "value": "[parameters('fslogixContainerType')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "managementVirtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + }, + "netAppAccountName": { + "value": "[parameters('netAppAccountName')]" + }, + "netAppCapacityPoolName": { + "value": "[parameters('netAppCapacityPoolName')]" + }, + "organizationalUnitPath": { + "value": "[parameters('organizationalUnitPath')]" + }, + "resourceGroupManagement": { + "value": "[parameters('resourceGroupManagement')]" + }, + "securityPrincipalNames": { + "value": "[parameters('securityPrincipalNames')]" + }, + "smbServerLocation": { + "value": "[parameters('smbServerLocation')]" + }, + "storageSku": { + "value": "[parameters('storageSku')]" + }, + "storageService": { + "value": "[parameters('storageService')]" + }, + "tagsNetAppAccount": { + "value": "[variables('tagsNetAppAccount')]" + }, + "tagsVirtualMachines": { + "value": "[variables('tagsVirtualMachines')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "deploymentUserAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12556264923235723564" + } + }, + "parameters": { + "artifactsUri": { + "type": "string" + }, + "activeDirectoryConnection": { + "type": "string" + }, + "delegatedSubnetId": { + "type": "string" + }, + "deploymentUserAssignedIdentityClientId": { + "type": "string" + }, + "dnsServers": { + "type": "string" + }, + "domainJoinPassword": { + "type": "securestring" + }, + "domainJoinUserPrincipalName": { + "type": "string" + }, + "domainName": { + "type": "string" + }, + "fileShares": { + "type": "array" + }, + "fslogixContainerType": { + "type": "string" + }, + "location": { + "type": "string" + }, + "managementVirtualMachineName": { + "type": "string" + }, + "netAppAccountName": { + "type": "string" + }, + "netAppCapacityPoolName": { + "type": "string" + }, + "organizationalUnitPath": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "securityPrincipalNames": { + "type": "array" + }, + "smbServerLocation": { + "type": "string" + }, + "storageSku": { + "type": "string" + }, + "storageService": { + "type": "string" + }, + "tagsNetAppAccount": { + "type": "object" + }, + "tagsVirtualMachines": { + "type": "object" + }, + "timestamp": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.NetApp/netAppAccounts", + "apiVersion": "2021-06-01", + "name": "[parameters('netAppAccountName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsNetAppAccount')]", + "properties": { + "activeDirectories": "[if(equals(parameters('activeDirectoryConnection'), 'false'), null(), createArray(createObject('aesEncryption', true(), 'domain', parameters('domainName'), 'dns', parameters('dnsServers'), 'organizationalUnit', parameters('organizationalUnitPath'), 'password', parameters('domainJoinPassword'), 'smbServerName', format('anf-{0}', parameters('smbServerLocation')), 'username', split(parameters('domainJoinUserPrincipalName'), '@')[0])))]", + "encryption": { + "keySource": "Microsoft.NetApp" + } + } + }, + { + "type": "Microsoft.NetApp/netAppAccounts/capacityPools", + "apiVersion": "2021-06-01", + "name": "[format('{0}/{1}', parameters('netAppAccountName'), parameters('netAppCapacityPoolName'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsNetAppAccount')]", + "properties": { + "coolAccess": false, + "encryptionType": "Single", + "qosType": "Auto", + "serviceLevel": "[parameters('storageSku')]", + "size": 4398046511104 + }, + "dependsOn": [ + "[resourceId('Microsoft.NetApp/netAppAccounts', parameters('netAppAccountName'))]" + ] + }, + { + "copy": { + "name": "volumes", + "count": "[length(range(0, length(parameters('fileShares'))))]" + }, + "type": "Microsoft.NetApp/netAppAccounts/capacityPools/volumes", + "apiVersion": "2021-06-01", + "name": "[format('{0}/{1}/{2}', parameters('netAppAccountName'), parameters('netAppCapacityPoolName'), parameters('fileShares')[range(0, length(parameters('fileShares')))[copyIndex()]])]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsNetAppAccount')]", + "properties": { + "avsDataStore": "Disabled", + "coolAccess": false, + "creationToken": "[parameters('fileShares')[range(0, length(parameters('fileShares')))[copyIndex()]]]", + "defaultGroupQuotaInKiBs": 0, + "defaultUserQuotaInKiBs": 0, + "encryptionKeySource": "Microsoft.NetApp", + "isDefaultQuotaEnabled": false, + "kerberosEnabled": false, + "ldapEnabled": false, + "networkFeatures": "Standard", + "protocolTypes": [ + "CIFS" + ], + "securityStyle": "ntfs", + "serviceLevel": "[parameters('storageSku')]", + "smbContinuouslyAvailable": true, + "smbEncryption": true, + "snapshotDirectoryVisible": true, + "subnetId": "[parameters('delegatedSubnetId')]", + "usageThreshold": 107374182400 + }, + "dependsOn": [ + "[resourceId('Microsoft.NetApp/netAppAccounts/capacityPools', parameters('netAppAccountName'), parameters('netAppCapacityPoolName'))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('FslogixNtfsPermissions_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileUris": { + "value": [ + "[format('{0}Set-NtfsPermissions.ps1', parameters('artifactsUri'))]" + ] + }, + "location": { + "value": "[parameters('location')]" + }, + "parameters": { + "value": "[format('-domainJoinPassword \"{0}\" -domainJoinUserPrincipalName {1} -fslogixContainerType {2} -securityPrincipalNames \"{3}\" -smbServerLocation {4} -storageService {5}', parameters('domainJoinPassword'), parameters('domainJoinUserPrincipalName'), parameters('fslogixContainerType'), parameters('securityPrincipalNames'), parameters('smbServerLocation'), parameters('storageService'))]" + }, + "scriptFileName": { + "value": "Set-NtfsPermissions.ps1" + }, + "tags": { + "value": "[parameters('tagsVirtualMachines')]" + }, + "userAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + }, + "virtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12496195847910288876" + } + }, + "parameters": { + "fileUris": { + "type": "array" + }, + "location": { + "type": "string" + }, + "parameters": { + "type": "securestring" + }, + "scriptFileName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]" + }, + "userAssignedIdentityClientId": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), 'CustomScriptExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "timestamp": "[parameters('timestamp')]" + }, + "protectedSettings": { + "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File {0} {1}', parameters('scriptFileName'), parameters('parameters'))]", + "fileUris": "[parameters('fileUris')]", + "managedIdentity": { + "clientId": "[parameters('userAssignedIdentityClientId')]" + } + } + } + } + ], + "outputs": { + "value": { + "type": "object", + "value": "[json(filter(reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'CustomScriptExtension'), '2021-03-01').instanceView.substatuses, lambda('item', equals(lambdaVariables('item').code, 'ComponentStatus/StdOut/succeeded')))[0].message)]" + } + } + } + }, + "dependsOn": [ + "volumes" + ] + } + ], + "outputs": { + "fileShares": { + "type": "array", + "value": "[if(contains(parameters('fslogixContainerType'), 'Office'), createArray(reference(resourceId('Microsoft.NetApp/netAppAccounts/capacityPools/volumes', parameters('netAppAccountName'), parameters('netAppCapacityPoolName'), parameters('fileShares')[range(0, length(parameters('fileShares')))[0]]), '2021-06-01').mountTargets[0].smbServerFqdn, reference(resourceId('Microsoft.NetApp/netAppAccounts/capacityPools/volumes', parameters('netAppAccountName'), parameters('netAppCapacityPoolName'), parameters('fileShares')[range(0, length(parameters('fileShares')))[1]]), '2021-06-01').mountTargets[0].smbServerFqdn), createArray(reference(resourceId('Microsoft.NetApp/netAppAccounts/capacityPools/volumes', parameters('netAppAccountName'), parameters('netAppCapacityPoolName'), parameters('fileShares')[range(0, length(parameters('fileShares')))[0]]), '2021-06-01').mountTargets[0].smbServerFqdn))]" + } + } + } + } + }, + { + "condition": "[and(equals(parameters('storageService'), 'AzureFiles'), contains(parameters('activeDirectorySolution'), 'DomainServices'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('AzureFiles_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupStorage')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "activeDirectorySolution": { + "value": "[parameters('activeDirectorySolution')]" + }, + "artifactsUri": { + "value": "[parameters('artifactsUri')]" + }, + "automationAccountName": { + "value": "[parameters('automationAccountName')]" + }, + "availability": { + "value": "[parameters('availability')]" + }, + "azureFilesPrivateDnsZoneResourceId": { + "value": "[parameters('azureFilesPrivateDnsZoneResourceId')]" + }, + "deploymentUserAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + }, + "domainJoinPassword": { + "value": "[parameters('domainJoinPassword')]" + }, + "domainJoinUserPrincipalName": { + "value": "[parameters('domainJoinUserPrincipalName')]" + }, + "enableRecoveryServices": { + "value": "[parameters('recoveryServices')]" + }, + "encryptionUserAssignedIdentityResourceId": { + "value": "[parameters('encryptionUserAssignedIdentityResourceId')]" + }, + "fileShares": { + "value": "[parameters('fileShares')]" + }, + "fslogixContainerType": { + "value": "[parameters('fslogixContainerType')]" + }, + "fslogixShareSizeInGB": { + "value": "[parameters('fslogixShareSizeInGB')]" + }, + "fslogixStorageService": { + "value": "[parameters('fslogixStorageService')]" + }, + "hostPoolType": { + "value": "[parameters('hostPoolType')]" + }, + "keyVaultUri": { + "value": "[parameters('keyVaultUri')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "managementVirtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + }, + "netbios": { + "value": "[parameters('netbios')]" + }, + "organizationalUnitPath": { + "value": "[parameters('organizationalUnitPath')]" + }, + "recoveryServicesVaultName": { + "value": "[parameters('recoveryServicesVaultName')]" + }, + "resourceGroupManagement": { + "value": "[parameters('resourceGroupManagement')]" + }, + "resourceGroupStorage": { + "value": "[parameters('resourceGroupStorage')]" + }, + "securityPrincipalNames": { + "value": "[parameters('securityPrincipalNames')]" + }, + "securityPrincipalObjectIds": { + "value": "[parameters('securityPrincipalObjectIds')]" + }, + "storageAccountNamePrefix": { + "value": "[parameters('storageAccountNamePrefix')]" + }, + "storageCount": { + "value": "[parameters('storageCount')]" + }, + "storageEncryptionKeyName": { + "value": "[parameters('storageEncryptionKeyName')]" + }, + "storageIndex": { + "value": "[parameters('storageIndex')]" + }, + "storageService": { + "value": "[parameters('storageService')]" + }, + "storageSku": { + "value": "[parameters('storageSku')]" + }, + "subnet": { + "value": "[parameters('subnet')]" + }, + "tagsAutomationAccounts": { + "value": "[variables('tagsAutomationAccounts')]" + }, + "tagsPrivateEndpoints": { + "value": "[variables('tagsPrivateEndpoints')]" + }, + "tagsRecoveryServicesVault": { + "value": "[variables('tagsRecoveryServicesVault')]" + }, + "tagsStorageAccounts": { + "value": "[variables('tagsStorageAccounts')]" + }, + "tagsVirtualMachines": { + "value": "[variables('tagsVirtualMachines')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "timeZone": { + "value": "[parameters('timeZone')]" + }, + "virtualNetwork": { + "value": "[parameters('virtualNetwork')]" + }, + "virtualNetworkResourceGroup": { + "value": "[parameters('virtualNetworkResourceGroup')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "14793803037488919131" + } + }, + "parameters": { + "artifactsUri": { + "type": "string" + }, + "automationAccountName": { + "type": "string" + }, + "availability": { + "type": "string" + }, + "azureFilesPrivateDnsZoneResourceId": { + "type": "string" + }, + "deploymentUserAssignedIdentityClientId": { + "type": "string" + }, + "domainJoinPassword": { + "type": "securestring" + }, + "domainJoinUserPrincipalName": { + "type": "string" + }, + "enableRecoveryServices": { + "type": "bool" + }, + "encryptionUserAssignedIdentityResourceId": { + "type": "string" + }, + "activeDirectorySolution": { + "type": "string" + }, + "fileShares": { + "type": "array" + }, + "fslogixShareSizeInGB": { + "type": "int" + }, + "fslogixContainerType": { + "type": "string" + }, + "fslogixStorageService": { + "type": "string" + }, + "hostPoolType": { + "type": "string" + }, + "keyVaultUri": { + "type": "string" + }, + "location": { + "type": "string" + }, + "managementVirtualMachineName": { + "type": "string" + }, + "netbios": { + "type": "string" + }, + "organizationalUnitPath": { + "type": "string" + }, + "recoveryServicesVaultName": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "resourceGroupStorage": { + "type": "string" + }, + "securityPrincipalObjectIds": { + "type": "array" + }, + "securityPrincipalNames": { + "type": "array" + }, + "storageAccountNamePrefix": { + "type": "string", + "minLength": 3 + }, + "storageCount": { + "type": "int" + }, + "storageEncryptionKeyName": { + "type": "string" + }, + "storageIndex": { + "type": "int" + }, + "storageSku": { + "type": "string" + }, + "storageService": { + "type": "string" + }, + "subnet": { + "type": "string" + }, + "tagsAutomationAccounts": { + "type": "object" + }, + "tagsPrivateEndpoints": { + "type": "object" + }, + "tagsRecoveryServicesVault": { + "type": "object" + }, + "tagsStorageAccounts": { + "type": "object" + }, + "tagsVirtualMachines": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "virtualNetwork": { + "type": "string" + }, + "virtualNetworkResourceGroup": { + "type": "string" + } + }, + "variables": { + "roleDefinitionId": "0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb", + "smbMultiChannel": { + "multichannel": { + "enabled": true + } + }, + "smbSettings": { + "versions": "SMB3.1.1;", + "authenticationMethods": "NTLMv2;Kerberos;", + "kerberosTicketEncryption": "AES-256;", + "channelEncryption": "AES-128-GCM;AES-256-GCM;" + }, + "storageRedundancy": "[if(equals(parameters('availability'), 'availabilityZones'), '_ZRS', '_LRS')]", + "subnetId": "[resourceId(parameters('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetwork'), parameters('subnet'))]" + }, + "resources": [ + { + "copy": { + "name": "storageAccounts", + "count": "[length(range(0, parameters('storageCount')))]" + }, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-09-01", + "name": "[format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[copyIndex()], parameters('storageIndex')), 2, '0'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsStorageAccounts')]", + "sku": { + "name": "[format('{0}{1}', parameters('storageSku'), variables('storageRedundancy'))]" + }, + "kind": "[if(equals(parameters('storageSku'), 'Standard'), 'StorageV2', 'FileStorage')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', parameters('encryptionUserAssignedIdentityResourceId'))]": {} + } + }, + "properties": { + "accessTier": "Hot", + "allowBlobPublicAccess": false, + "allowCrossTenantReplication": false, + "allowedCopyScope": "PrivateLink", + "allowSharedKeyAccess": true, + "azureFilesIdentityBasedAuthentication": { + "directoryServiceOptions": "[if(equals(parameters('activeDirectorySolution'), 'MicrosoftEntraDomainServices'), 'AADDS', 'None')]" + }, + "defaultToOAuthAuthentication": false, + "dnsEndpointType": "Standard", + "encryption": { + "identity": { + "userAssignedIdentity": "[parameters('encryptionUserAssignedIdentityResourceId')]" + }, + "requireInfrastructureEncryption": true, + "keyvaultproperties": { + "keyvaulturi": "[parameters('keyVaultUri')]", + "keyname": "[parameters('storageEncryptionKeyName')]" + }, + "services": "[if(equals(parameters('storageSku'), 'Standard'), createObject('file', createObject('keyType', 'Account', 'enabled', true()), 'table', createObject('keyType', 'Account', 'enabled', true()), 'queue', createObject('keyType', 'Account', 'enabled', true()), 'blob', createObject('keyType', 'Account', 'enabled', true())), createObject('file', createObject('keyType', 'Account', 'enabled', true())))]", + "keySource": "Microsoft.KeyVault" + }, + "largeFileSharesState": "[if(equals(parameters('storageSku'), 'Standard'), 'Enabled', null())]", + "minimumTlsVersion": "TLS1_2", + "networkAcls": { + "bypass": "AzureServices", + "virtualNetworkRules": [], + "ipRules": [], + "defaultAction": "Deny" + }, + "publicNetworkAccess": "Disabled", + "supportsHttpsTrafficOnly": true + } + }, + { + "copy": { + "name": "roleAssignment", + "count": "[length(range(0, parameters('storageCount')))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')))]", + "name": "[guid(parameters('securityPrincipalObjectIds')[range(0, parameters('storageCount'))[copyIndex()]], variables('roleDefinitionId'), resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0'))))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitionId'))]", + "principalId": "[parameters('securityPrincipalObjectIds')[range(0, parameters('storageCount'))[copyIndex()]]]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')))]", + "[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')))]" + ] + }, + { + "copy": { + "name": "fileServices", + "count": "[length(range(0, parameters('storageCount')))]" + }, + "type": "Microsoft.Storage/storageAccounts/fileServices", + "apiVersion": "2022-09-01", + "name": "[format('{0}/{1}', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')), 'default')]", + "properties": { + "protocolSettings": { + "smb": "[if(equals(parameters('storageSku'), 'Standard'), variables('smbSettings'), union(variables('smbSettings'), variables('smbMultiChannel')))]" + }, + "shareDeleteRetentionPolicy": { + "enabled": false + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')))]" + ] + }, + { + "copy": { + "name": "privateEndpoints", + "count": "[length(range(0, parameters('storageCount')))]" + }, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-04-01", + "name": "[format('pe-{0}{1}-file', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[copyIndex()], parameters('storageIndex')), 2, '0'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsPrivateEndpoints')]", + "properties": { + "customNetworkInterfaceName": "[format('nic-{0}{1}-file', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[copyIndex()], parameters('storageIndex')), 2, '0'))]", + "privateLinkServiceConnections": [ + { + "name": "[format('pe-{0}', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')))]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')))]", + "groupIds": [ + "file" + ] + } + } + ], + "subnet": { + "id": "[variables('subnetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')))]", + "[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')))]" + ] + }, + { + "copy": { + "name": "privateDnsZoneGroups", + "count": "[length(range(0, parameters('storageCount')))]" + }, + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-08-01", + "name": "[format('{0}/{1}', format('pe-{0}{1}-file', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')), format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[copyIndex()], parameters('storageIndex')), 2, '0')))]", + "properties": { + "privateDnsZoneConfigs": [ + { + "name": "ipconfig1", + "properties": { + "privateDnsZoneId": "[parameters('azureFilesPrivateDnsZoneResourceId')]" + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', format('pe-{0}{1}-file', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')))]", + "storageAccounts" + ] + }, + { + "copy": { + "name": "shares", + "count": "[length(range(0, parameters('storageCount')))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('fileShares_{0}_{1}', range(0, parameters('storageCount'))[copyIndex()], parameters('timestamp'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileShares": { + "value": "[parameters('fileShares')]" + }, + "fslogixShareSizeInGB": { + "value": "[parameters('fslogixShareSizeInGB')]" + }, + "storageAccountName": { + "value": "[format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0'))]" + }, + "storageSku": { + "value": "[parameters('storageSku')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "14159020950234848511" + } + }, + "parameters": { + "fileShares": { + "type": "array" + }, + "fslogixShareSizeInGB": { + "type": "int" + }, + "storageAccountName": { + "type": "string" + }, + "storageSku": { + "type": "string" + } + }, + "resources": [ + { + "copy": { + "name": "shares", + "count": "[length(range(0, length(parameters('fileShares'))))]" + }, + "type": "Microsoft.Storage/storageAccounts/fileServices/shares", + "apiVersion": "2022-09-01", + "name": "[format('{0}/default/{1}', parameters('storageAccountName'), parameters('fileShares')[range(0, length(parameters('fileShares')))[copyIndex()]])]", + "properties": { + "accessTier": "[if(equals(parameters('storageSku'), 'Premium'), 'Premium', 'TransactionOptimized')]", + "shareQuota": "[parameters('fslogixShareSizeInGB')]", + "enabledProtocols": "SMB" + } + } + ] + } + }, + "dependsOn": [ + "roleAssignment", + "[resourceId('Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')))]" + ] + }, + { + "condition": "[contains(parameters('activeDirectorySolution'), 'DomainServices')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('FslogixNtfsPermissions_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileUris": { + "value": [ + "[format('{0}Set-NtfsPermissions.ps1', parameters('artifactsUri'))]" + ] + }, + "location": { + "value": "[parameters('location')]" + }, + "parameters": { + "value": "[format('-domainJoinPassword \"{0}\" -domainJoinUserPrincipalName {1} -activeDirectorySolution {2} -Environment {3} -fslogixContainerType {4} -netbios {5} -organizationalUnitPath \"{6}\" -securityPrincipalNames \"{7}\" -StorageAccountPrefix {8} -StorageAccountResourceGroupName {9} -storageCount {10} -storageIndex {11} -storageService {12} -StorageSuffix {13} -SubscriptionId {14} -TenantId {15} -UserAssignedIdentityClientId {16}', parameters('domainJoinPassword'), parameters('domainJoinUserPrincipalName'), parameters('activeDirectorySolution'), environment().name, parameters('fslogixContainerType'), parameters('netbios'), parameters('organizationalUnitPath'), parameters('securityPrincipalNames'), parameters('storageAccountNamePrefix'), parameters('resourceGroupStorage'), parameters('storageCount'), parameters('storageIndex'), parameters('storageService'), environment().suffixes.storage, subscription().subscriptionId, subscription().tenantId, parameters('deploymentUserAssignedIdentityClientId'))]" + }, + "scriptFileName": { + "value": "Set-NtfsPermissions.ps1" + }, + "tags": { + "value": "[parameters('tagsVirtualMachines')]" + }, + "userAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + }, + "virtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12496195847910288876" + } + }, + "parameters": { + "fileUris": { + "type": "array" + }, + "location": { + "type": "string" + }, + "parameters": { + "type": "securestring" + }, + "scriptFileName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]" + }, + "userAssignedIdentityClientId": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), 'CustomScriptExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "timestamp": "[parameters('timestamp')]" + }, + "protectedSettings": { + "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File {0} {1}', parameters('scriptFileName'), parameters('parameters'))]", + "fileUris": "[parameters('fileUris')]", + "managedIdentity": { + "clientId": "[parameters('userAssignedIdentityClientId')]" + } + } + } + } + ], + "outputs": { + "value": { + "type": "object", + "value": "[json(filter(reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'CustomScriptExtension'), '2021-03-01').instanceView.substatuses, lambda('item', equals(lambdaVariables('item').code, 'ComponentStatus/StdOut/succeeded')))[0].message)]" + } + } + } + }, + "dependsOn": [ + "privateDnsZoneGroups", + "privateEndpoints", + "shares" + ] + }, + { + "condition": "[and(parameters('enableRecoveryServices'), contains(parameters('hostPoolType'), 'Pooled'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('recoveryServices_AzureFiles_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileShares": { + "value": "[parameters('fileShares')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "recoveryServicesVaultName": { + "value": "[parameters('recoveryServicesVaultName')]" + }, + "resourceGroupStorage": { + "value": "[parameters('resourceGroupStorage')]" + }, + "storageAccountNamePrefix": { + "value": "[parameters('storageAccountNamePrefix')]" + }, + "storageCount": { + "value": "[parameters('storageCount')]" + }, + "storageIndex": { + "value": "[parameters('storageIndex')]" + }, + "tagsRecoveryServicesVault": { + "value": "[parameters('tagsRecoveryServicesVault')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "1687915397771494402" + } + }, + "parameters": { + "fileShares": { + "type": "array" + }, + "location": { + "type": "string" + }, + "recoveryServicesVaultName": { + "type": "string" + }, + "resourceGroupStorage": { + "type": "string" + }, + "storageAccountNamePrefix": { + "type": "string" + }, + "storageCount": { + "type": "int" + }, + "storageIndex": { + "type": "int" + }, + "tagsRecoveryServicesVault": { + "type": "object" + }, + "timestamp": { + "type": "string" + } + }, + "resources": [ + { + "copy": { + "name": "protectionContainers", + "count": "[length(range(0, parameters('storageCount')))]" + }, + "type": "Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers", + "apiVersion": "2022-03-01", + "name": "[format('{0}/Azure/storagecontainer;Storage;{1};{2}{3}', parameters('recoveryServicesVaultName'), parameters('resourceGroupStorage'), parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[copyIndex()], parameters('storageIndex')), 2, '0'))]", + "properties": { + "backupManagementType": "AzureStorage", + "containerType": "StorageContainer", + "sourceResourceId": "[resourceId(parameters('resourceGroupStorage'), 'Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[copyIndex()], parameters('storageIndex')), 2, '0')))]" + } + }, + { + "copy": { + "name": "protectedItems_fileShares", + "count": "[length(range(0, parameters('storageCount')))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('BackupProtectedItems_fileShares_{0}_{1}', add(range(0, parameters('storageCount'))[copyIndex()], parameters('storageIndex')), parameters('timestamp'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileShares": { + "value": "[parameters('fileShares')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "protectionContainerName": { + "value": "[format('{0}/Azure/storagecontainer;Storage;{1};{2}{3}', parameters('recoveryServicesVaultName'), parameters('resourceGroupStorage'), parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0'))]" + }, + "policyId": { + "value": "[resourceId('Microsoft.RecoveryServices/vaults/backupPolicies', parameters('recoveryServicesVaultName'), 'AvdPolicyStorage')]" + }, + "sourceResourceId": { + "value": "[resourceId(parameters('resourceGroupStorage'), 'Microsoft.Storage/storageAccounts', format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[copyIndex()], parameters('storageIndex')), 2, '0')))]" + }, + "tags": { + "value": "[parameters('tagsRecoveryServicesVault')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "3962302755820580995" + } + }, + "parameters": { + "fileShares": { + "type": "array" + }, + "location": { + "type": "string" + }, + "policyId": { + "type": "string" + }, + "protectionContainerName": { + "type": "string" + }, + "sourceResourceId": { + "type": "string" + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "copy": { + "name": "protectedItems_FileShare", + "count": "[length(parameters('fileShares'))]" + }, + "condition": "[contains(parameters('fileShares')[copyIndex()], 'profile')]", + "type": "Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems", + "apiVersion": "2022-03-01", + "name": "[format('{0}/AzureFileShare;{1}', parameters('protectionContainerName'), parameters('fileShares')[copyIndex()])]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "protectedItemType": "AzureFileShareProtectedItem", + "policyId": "[parameters('policyId')]", + "sourceResourceId": "[parameters('sourceResourceId')]" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers', split(format('{0}/Azure/storagecontainer;Storage;{1};{2}{3}', parameters('recoveryServicesVaultName'), parameters('resourceGroupStorage'), parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')), '/')[0], split(format('{0}/Azure/storagecontainer;Storage;{1};{2}{3}', parameters('recoveryServicesVaultName'), parameters('resourceGroupStorage'), parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')), '/')[1], split(format('{0}/Azure/storagecontainer;Storage;{1};{2}{3}', parameters('recoveryServicesVaultName'), parameters('resourceGroupStorage'), parameters('storageAccountNamePrefix'), padLeft(add(range(0, parameters('storageCount'))[range(0, parameters('storageCount'))[copyIndex()]], parameters('storageIndex')), 2, '0')), '/')[2])]" + ] + } + ] + } + }, + "dependsOn": [ + "shares" + ] + }, + { + "condition": "[and(equals(parameters('fslogixStorageService'), 'AzureFiles Premium'), greater(parameters('storageCount'), 0))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('AutoIncreasePremiumFileShareQuota_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "artifactsUri": { + "value": "[parameters('artifactsUri')]" + }, + "automationAccountName": { + "value": "[parameters('automationAccountName')]" + }, + "deploymentUserAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + }, + "fslogixContainerType": { + "value": "[parameters('fslogixContainerType')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "managementVirtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + }, + "storageAccountNamePrefix": { + "value": "[parameters('storageAccountNamePrefix')]" + }, + "storageCount": { + "value": "[parameters('storageCount')]" + }, + "storageIndex": { + "value": "[parameters('storageIndex')]" + }, + "storageResourceGroupName": { + "value": "[parameters('resourceGroupStorage')]" + }, + "tags": { + "value": "[parameters('tagsAutomationAccounts')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "timeZone": { + "value": "[parameters('timeZone')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "16369972078902188613" + } + }, + "parameters": { + "artifactsUri": { + "type": "string" + }, + "automationAccountName": { + "type": "string" + }, + "deploymentUserAssignedIdentityClientId": { + "type": "string" + }, + "fslogixContainerType": { + "type": "string" + }, + "location": { + "type": "string" + }, + "managementVirtualMachineName": { + "type": "string" + }, + "storageAccountNamePrefix": { + "type": "string" + }, + "storageCount": { + "type": "int" + }, + "storageIndex": { + "type": "int" + }, + "storageResourceGroupName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "timeZone": { + "type": "string" + } + }, + "variables": { + "runbookFileName": "Set-FileShareScaling.ps1", + "scriptFileName": "Set-AutomationRunbook.ps1", + "subscriptionId": "[subscription().subscriptionId]" + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Runbook_QuotaScaling_{0}', parameters('timestamp'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileUris": { + "value": [ + "[format('{0}{1}', parameters('artifactsUri'), variables('runbookFileName'))]", + "[format('{0}{1}', parameters('artifactsUri'), variables('scriptFileName'))]" + ] + }, + "location": { + "value": "[parameters('location')]" + }, + "parameters": { + "value": "[format('-AutomationAccountName {0} -Environment {1} -ResourceGroupName {2} -RunbookFileName {3} -SubscriptionId {4} -TenantId {5} -UserAssignedIdentityClientId {6}', parameters('automationAccountName'), environment().name, resourceGroup().name, variables('runbookFileName'), subscription().subscriptionId, tenant().tenantId, parameters('deploymentUserAssignedIdentityClientId'))]" + }, + "scriptFileName": { + "value": "[variables('scriptFileName')]" + }, + "tags": "[if(contains(parameters('tags'), 'Microsoft.Compute/virtualMachines'), createObject('value', parameters('tags')['Microsoft.Compute/virtualMachines']), createObject('value', createObject()))]", + "userAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + }, + "virtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12496195847910288876" + } + }, + "parameters": { + "fileUris": { + "type": "array" + }, + "location": { + "type": "string" + }, + "parameters": { + "type": "securestring" + }, + "scriptFileName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]" + }, + "userAssignedIdentityClientId": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), 'CustomScriptExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "timestamp": "[parameters('timestamp')]" + }, + "protectedSettings": { + "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File {0} {1}', parameters('scriptFileName'), parameters('parameters'))]", + "fileUris": "[parameters('fileUris')]", + "managedIdentity": { + "clientId": "[parameters('userAssignedIdentityClientId')]" + } + } + } + } + ], + "outputs": { + "value": { + "type": "object", + "value": "[json(filter(reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'CustomScriptExtension'), '2021-03-01').instanceView.substatuses, lambda('item', equals(lambdaVariables('item').code, 'ComponentStatus/StdOut/succeeded')))[0].message)]" + } + } + } + } + }, + { + "copy": { + "name": "schedules", + "count": "[length(range(parameters('storageIndex'), parameters('storageCount')))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Schedules_{0}_{1}', range(parameters('storageIndex'), parameters('storageCount'))[copyIndex()], parameters('timestamp'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "automationAccountName": { + "value": "[parameters('automationAccountName')]" + }, + "fslogixContainerType": { + "value": "[parameters('fslogixContainerType')]" + }, + "storageAccountName": { + "value": "[format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(range(parameters('storageIndex'), parameters('storageCount'))[copyIndex()], 2, '0'))]" + }, + "timeZone": { + "value": "[parameters('timeZone')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "13477542919303037673" + } + }, + "parameters": { + "automationAccountName": { + "type": "string" + }, + "fslogixContainerType": { + "type": "string" + }, + "storageAccountName": { + "type": "string" + }, + "time": { + "type": "string", + "defaultValue": "[utcNow()]" + }, + "timeZone": { + "type": "string" + } + }, + "resources": [ + { + "copy": { + "name": "schedules_ProfileContainers", + "count": "[length(range(0, 4))]" + }, + "type": "Microsoft.Automation/automationAccounts/schedules", + "apiVersion": "2022-08-08", + "name": "[format('{0}/{1}', parameters('automationAccountName'), format('{0}_ProfileContainers_{1}min', parameters('storageAccountName'), mul(add(range(0, 4)[copyIndex()], 1), 15)))]", + "properties": { + "advancedSchedule": {}, + "description": null, + "expiryTime": null, + "frequency": "Hour", + "interval": 1, + "startTime": "[dateTimeAdd(parameters('time'), format('PT{0}M', mul(add(range(0, 4)[copyIndex()], 1), 15)))]", + "timeZone": "[parameters('timeZone')]" + } + }, + { + "copy": { + "name": "schedules_OfficeContainers", + "count": "[length(range(0, 4))]" + }, + "condition": "[contains(parameters('fslogixContainerType'), 'Office')]", + "type": "Microsoft.Automation/automationAccounts/schedules", + "apiVersion": "2022-08-08", + "name": "[format('{0}/{1}', parameters('automationAccountName'), format('{0}_OfficeContainers_{1}min', parameters('storageAccountName'), mul(add(range(0, 4)[copyIndex()], 1), 15)))]", + "properties": { + "advancedSchedule": {}, + "description": null, + "expiryTime": null, + "frequency": "Hour", + "interval": 1, + "startTime": "[dateTimeAdd(parameters('time'), format('PT{0}M', mul(add(range(0, 4)[copyIndex()], 1), 15)))]", + "timeZone": "[parameters('timeZone')]" + } + } + ] + } + } + }, + { + "copy": { + "name": "jobSchedules", + "count": "[length(range(parameters('storageIndex'), parameters('storageCount')))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('JobSchedules_{0}_{1}', range(parameters('storageIndex'), parameters('storageCount'))[copyIndex()], parameters('timestamp'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "automationAccountName": { + "value": "[parameters('automationAccountName')]" + }, + "environment": { + "value": "[environment().name]" + }, + "fslogixContainerType": { + "value": "[parameters('fslogixContainerType')]" + }, + "runbookName": { + "value": "[replace(variables('runbookFileName'), '.ps1', '')]" + }, + "resourceGroupName": { + "value": "[parameters('storageResourceGroupName')]" + }, + "storageAccountName": { + "value": "[format('{0}{1}', parameters('storageAccountNamePrefix'), padLeft(range(parameters('storageIndex'), parameters('storageCount'))[copyIndex()], 2, '0'))]" + }, + "subscriptionId": { + "value": "[variables('subscriptionId')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "422798455992317931" + } + }, + "parameters": { + "automationAccountName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "fslogixContainerType": { + "type": "string" + }, + "resourceGroupName": { + "type": "string" + }, + "runbookName": { + "type": "string" + }, + "storageAccountName": { + "type": "string" + }, + "subscriptionId": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + }, + "resources": [ + { + "copy": { + "name": "jobSchedules_ProfileContainers", + "count": "[length(range(0, 4))]" + }, + "type": "Microsoft.Automation/automationAccounts/jobSchedules", + "apiVersion": "2022-08-08", + "name": "[format('{0}/{1}', parameters('automationAccountName'), guid(parameters('timestamp'), parameters('runbookName'), parameters('storageAccountName'), 'ProfileContainers', string(range(0, 4)[copyIndex()])))]", + "properties": { + "parameters": { + "environment": "[parameters('environment')]", + "FileShareName": "profile-containers", + "resourceGroupName": "[parameters('resourceGroupName')]", + "storageAccountName": "[parameters('storageAccountName')]", + "subscriptionId": "[parameters('subscriptionId')]" + }, + "runbook": { + "name": "[parameters('runbookName')]" + }, + "runOn": null, + "schedule": { + "name": "[format('{0}_ProfileContainers_{1}min', parameters('storageAccountName'), mul(add(range(0, 4)[copyIndex()], 1), 15))]" + } + } + }, + { + "copy": { + "name": "jobSchedules_OfficeContainers", + "count": "[length(range(0, 4))]" + }, + "condition": "[contains(parameters('fslogixContainerType'), 'Office')]", + "type": "Microsoft.Automation/automationAccounts/jobSchedules", + "apiVersion": "2022-08-08", + "name": "[format('{0}/{1}', parameters('automationAccountName'), guid(parameters('timestamp'), parameters('runbookName'), parameters('storageAccountName'), 'OfficeContainers', string(range(0, 4)[copyIndex()])))]", + "properties": { + "parameters": { + "environment": "[parameters('environment')]", + "FileShareName": "office-containers", + "resourceGroupName": "[parameters('resourceGroupName')]", + "storageAccountName": "[parameters('storageAccountName')]", + "subscriptionId": "[parameters('subscriptionId')]" + }, + "runbook": { + "name": "[parameters('runbookName')]" + }, + "runOn": null, + "schedule": { + "name": "[format('{0}_OfficeContainers_{1}min', parameters('storageAccountName'), mul(add(range(0, 4)[copyIndex()], 1), 15))]" + } + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('Runbook_QuotaScaling_{0}', parameters('timestamp')))]", + "schedules" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('RoleAssignment_Storage_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('storageResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "PrincipalId": { + "value": "[reference(resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName')), '2022-08-08', 'full').identity.principalId]" + }, + "PrincipalType": { + "value": "ServicePrincipal" + }, + "RoleDefinitionId": { + "value": "17d1049b-9a84-46fb-8f53-869881c3d3ab" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "3999918654980032531" + } + }, + "parameters": { + "PrincipalId": { + "type": "string" + }, + "PrincipalType": { + "type": "string" + }, + "RoleDefinitionId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('PrincipalId'), parameters('RoleDefinitionId'), resourceGroup().id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('RoleDefinitionId'))]", + "principalId": "[parameters('PrincipalId')]", + "principalType": "[parameters('PrincipalType')]" + } + } + ] + } + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('FslogixNtfsPermissions_{0}', parameters('timestamp')))]" + ] + } + ] + } + } + } + ], + "outputs": { + "netAppShares": { + "type": "array", + "value": "[if(equals(parameters('storageService'), 'AzureNetAppFiles'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupStorage')), 'Microsoft.Resources/deployments', format('AzureNetAppFiles_{0}', parameters('timestamp'))), '2022-09-01').outputs.fileShares.value, createArray('None'))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/deployments', format('ControlPlane_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Network_Hosts_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp')))]", + "rgs" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('SessionHosts_{0}', parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "acceleratedNetworking": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.validateAcceleratedNetworking.value]" + }, + "activeDirectorySolution": { + "value": "[parameters('activeDirectorySolution')]" + }, + "artifactsUri": { + "value": "[variables('artifactsUri')]" + }, + "artifactsUserAssignedIdentityClientId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.artifactsUserAssignedIdentityClientId.value]" + }, + "artifactsUserAssignedIdentityResourceId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.artifactsUserAssignedIdentityResourceId.value]" + }, + "automationAccountName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.automationAccountName.value]" + }, + "availability": { + "value": "[parameters('availability')]" + }, + "availabilitySetNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.availabilitySetNamePrefix.value]" + }, + "availabilitySetsCount": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.availabilitySetsCount.value]" + }, + "availabilitySetsIndex": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.beginAvSetRange.value]" + }, + "availabilityZones": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.validateAvailabilityZones.value]" + }, + "avdAgentBootLoaderMsiName": { + "value": "[parameters('avdAgentBootLoaderMsiName')]" + }, + "avdAgentMsiName": { + "value": "[parameters('avdAgentMsiName')]" + }, + "dataCollectionRuleAssociationName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.dataCollectionRuleAssociationName.value]" + }, + "dataCollectionRuleResourceId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.dataCollectionRuleResourceId.value]" + }, + "deploymentUserAssignedIdentityClientId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.deploymentUserAssignedIdentityClientId.value]" + }, + "diskEncryptionSetResourceId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.diskEncryptionSetResourceId.value]" + }, + "diskNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.diskNamePrefix.value]" + }, + "diskSku": { + "value": "[parameters('diskSku')]" + }, + "divisionRemainderValue": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.divisionRemainderValue.value]" + }, + "domainJoinPassword": { + "value": "[parameters('domainJoinPassword')]" + }, + "domainJoinUserPrincipalName": { + "value": "[parameters('domainJoinUserPrincipalName')]" + }, + "domainName": { + "value": "[parameters('domainName')]" + }, + "drainMode": { + "value": "[parameters('drainMode')]" + }, + "enableRecoveryServices": { + "value": "[parameters('recoveryServices')]" + }, + "enableScalingTool": { + "value": "[parameters('scalingTool')]" + }, + "fslogix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.fslogix.value]" + }, + "fslogixContainerType": { + "value": "[parameters('fslogixContainerType')]" + }, + "hostPoolName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.hostPoolName.value]" + }, + "hostPoolType": { + "value": "[parameters('hostPoolType')]" + }, + "hybridRunbookWorkerGroupName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.hybridRunbookWorkerGroupName.value]" + }, + "imageOffer": { + "value": "[parameters('imageOffer')]" + }, + "imagePublisher": { + "value": "[parameters('imagePublisher')]" + }, + "imageSku": { + "value": "[parameters('imageSku')]" + }, + "imageDefinitionResourceId": { + "value": "[parameters('imageDefinitionResourceId')]" + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "logAnalyticsWorkspaceName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.logAnalyticsWorkspaceName.value]" + }, + "managementVirtualMachineName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.virtualMachineName.value]" + }, + "maxResourcesPerTemplateDeployment": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.maxResourcesPerTemplateDeployment.value]" + }, + "monitoring": { + "value": "[parameters('monitoring')]" + }, + "netAppFileShares": "[if(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.fslogix.value, createObject('value', reference(subscriptionResourceId('Microsoft.Resources/deployments', format('FSLogix_{0}', parameters('timestamp'))), '2022-09-01').outputs.netAppShares.value), createObject('value', createArray('None')))]", + "networkInterfaceNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.networkInterfaceNamePrefix.value]" + }, + "organizationalUnitPath": { + "value": "[parameters('organizationalUnitPath')]" + }, + "pooledHostPool": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.pooledHostPool.value]" + }, + "recoveryServicesVaultName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.recoveryServicesVaultName.value]" + }, + "resourceGroupControlPlane": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupControlPlane.value]" + }, + "resourceGroupHosts": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupHosts.value]" + }, + "resourceGroupManagement": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupManagement.value]" + }, + "roleDefinitions": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.roleDefinitions.value]" + }, + "scalingBeginPeakTime": { + "value": "[parameters('scalingBeginPeakTime')]" + }, + "scalingEndPeakTime": { + "value": "[parameters('scalingEndPeakTime')]" + }, + "scalingLimitSecondsToForceLogOffUser": { + "value": "[parameters('scalingLimitSecondsToForceLogOffUser')]" + }, + "scalingMinimumNumberOfRdsh": { + "value": "[parameters('scalingMinimumNumberOfRdsh')]" + }, + "scalingSessionThresholdPerCPU": { + "value": "[parameters('scalingSessionThresholdPerCPU')]" + }, + "securityPrincipalObjectIds": { + "value": "[map(parameters('securityPrincipals'), lambda('item', lambdaVariables('item').objectId))]" + }, + "securityLogAnalyticsWorkspaceResourceId": { + "value": "[parameters('securityLogAnalyticsWorkspaceResourceId')]" + }, + "sessionHostBatchCount": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.sessionHostBatchCount.value]" + }, + "sessionHostIndex": { + "value": "[parameters('sessionHostIndex')]" + }, + "storageAccountPrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.storageAccountNamePrefix.value]" + }, + "storageCount": { + "value": "[parameters('storageCount')]" + }, + "storageIndex": { + "value": "[parameters('storageIndex')]" + }, + "storageService": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.storageService.value]" + }, + "storageSuffix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.storageSuffix.value]" + }, + "subnet": "[if(equals(length(variables('deploymentLocations')), 1), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[10]), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_Hosts_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[10]))]", + "tags": { + "value": "[parameters('tags')]" + }, + "timeDifference": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.timeDifference.value]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "timeZone": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp'))), '2022-09-01').outputs.timeZone.value]" + }, + "virtualMachineMonitoringAgent": { + "value": "[parameters('virtualMachineMonitoringAgent')]" + }, + "virtualMachineNamePrefix": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.virtualMachineNamePrefix.value]" + }, + "virtualMachinePassword": { + "value": "[parameters('virtualMachinePassword')]" + }, + "virtualMachineSize": { + "value": "[parameters('virtualMachineSize')]" + }, + "virtualMachineUsername": { + "value": "[parameters('virtualMachineUsername')]" + }, + "virtualNetwork": "[if(equals(length(variables('deploymentLocations')), 1), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[8]), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_Hosts_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[8]))]", + "virtualNetworkResourceGroup": "[if(equals(length(variables('deploymentLocations')), 1), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[4]), createObject('value', split(reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Network_Hosts_{0}', parameters('timestamp'))), '2022-09-01').outputs.subnetResourceId.value, '/')[4]))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "5923296477539659124" + } + }, + "parameters": { + "acceleratedNetworking": { + "type": "string" + }, + "activeDirectorySolution": { + "type": "string" + }, + "artifactsUri": { + "type": "string" + }, + "artifactsUserAssignedIdentityClientId": { + "type": "string" + }, + "artifactsUserAssignedIdentityResourceId": { + "type": "string" + }, + "automationAccountName": { + "type": "string" + }, + "availability": { + "type": "string" + }, + "availabilitySetNamePrefix": { + "type": "string" + }, + "availabilitySetsCount": { + "type": "int" + }, + "availabilitySetsIndex": { + "type": "int" + }, + "availabilityZones": { + "type": "array" + }, + "avdAgentBootLoaderMsiName": { + "type": "string" + }, + "avdAgentMsiName": { + "type": "string" + }, + "dataCollectionRuleAssociationName": { + "type": "string" + }, + "dataCollectionRuleResourceId": { + "type": "string" + }, + "deploymentUserAssignedIdentityClientId": { + "type": "string" + }, + "diskEncryptionSetResourceId": { + "type": "string" + }, + "diskNamePrefix": { + "type": "string" + }, + "diskSku": { + "type": "string" + }, + "divisionRemainderValue": { + "type": "int" + }, + "domainJoinPassword": { + "type": "securestring" + }, + "domainJoinUserPrincipalName": { + "type": "string" + }, + "domainName": { + "type": "string" + }, + "drainMode": { + "type": "bool" + }, + "fslogix": { + "type": "bool" + }, + "fslogixContainerType": { + "type": "string" + }, + "hostPoolName": { + "type": "string" + }, + "hostPoolType": { + "type": "string" + }, + "hybridRunbookWorkerGroupName": { + "type": "string" + }, + "imageDefinitionResourceId": { + "type": "string" + }, + "imageOffer": { + "type": "string" + }, + "imagePublisher": { + "type": "string" + }, + "imageSku": { + "type": "string" + }, + "location": { + "type": "string" + }, + "logAnalyticsWorkspaceName": { + "type": "string" + }, + "managementVirtualMachineName": { + "type": "string" + }, + "maxResourcesPerTemplateDeployment": { + "type": "int" + }, + "monitoring": { + "type": "bool" + }, + "netAppFileShares": { + "type": "array" + }, + "networkInterfaceNamePrefix": { + "type": "string" + }, + "organizationalUnitPath": { + "type": "string" + }, + "pooledHostPool": { + "type": "bool" + }, + "enableRecoveryServices": { + "type": "bool" + }, + "enableScalingTool": { + "type": "bool" + }, + "recoveryServicesVaultName": { + "type": "string" + }, + "resourceGroupControlPlane": { + "type": "string" + }, + "resourceGroupHosts": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "roleDefinitions": { + "type": "object" + }, + "scalingBeginPeakTime": { + "type": "string" + }, + "scalingEndPeakTime": { + "type": "string" + }, + "scalingLimitSecondsToForceLogOffUser": { + "type": "string" + }, + "scalingMinimumNumberOfRdsh": { + "type": "string" + }, + "scalingSessionThresholdPerCPU": { + "type": "string" + }, + "securityPrincipalObjectIds": { + "type": "array" + }, + "securityLogAnalyticsWorkspaceResourceId": { + "type": "string" + }, + "sessionHostBatchCount": { + "type": "int" + }, + "sessionHostIndex": { + "type": "int" + }, + "storageAccountPrefix": { + "type": "string" + }, + "storageCount": { + "type": "int" + }, + "storageIndex": { + "type": "int" + }, + "storageService": { + "type": "string" + }, + "storageSuffix": { + "type": "string" + }, + "subnet": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timeDifference": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "virtualMachineMonitoringAgent": { + "type": "string" + }, + "virtualMachineNamePrefix": { + "type": "string" + }, + "virtualMachinePassword": { + "type": "securestring" + }, + "virtualMachineSize": { + "type": "string" + }, + "virtualMachineUsername": { + "type": "string" + }, + "virtualNetwork": { + "type": "string" + }, + "virtualNetworkResourceGroup": { + "type": "string" + } + }, + "variables": { + "tagsAutomationAccounts": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupManagement'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Automation/automationAccounts'), parameters('tags')['Microsoft.Automation/automationAccounts'], createObject()))]", + "tagsAvailabilitySets": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupManagement'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Compute/availabilitySets'), parameters('tags')['Microsoft.Compute/availabilitySets'], createObject()))]", + "tagsNetworkInterfaces": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupManagement'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Network/networkInterfaces'), parameters('tags')['Microsoft.Network/networkInterfaces'], createObject()))]", + "tagsRecoveryServicesVault": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupManagement'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.recoveryServices/vaults'), parameters('tags')['Microsoft.recoveryServices/vaults'], createObject()))]", + "tagsVirtualMachines": "[union(createObject('cm-resource-parent', format('{0}}}/resourceGroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}', subscription().id, parameters('resourceGroupManagement'), parameters('hostPoolName'))), if(contains(parameters('tags'), 'Microsoft.Compute/virtualMachines'), parameters('tags')['Microsoft.Compute/virtualMachines'], createObject()))]" + }, + "resources": [ + { + "condition": "[and(parameters('pooledHostPool'), equals(parameters('availability'), 'availabilitySets'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('availabilitySets_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupHosts')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "availabilitySetNamePrefix": { + "value": "[parameters('availabilitySetNamePrefix')]" + }, + "availabilitySetsCount": { + "value": "[parameters('availabilitySetsCount')]" + }, + "availabilitySetsIndex": { + "value": "[parameters('availabilitySetsIndex')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tagsAvailabilitySets": { + "value": "[variables('tagsAvailabilitySets')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "840657672737095387" + } + }, + "parameters": { + "availabilitySetNamePrefix": { + "type": "string" + }, + "availabilitySetsCount": { + "type": "int" + }, + "availabilitySetsIndex": { + "type": "int" + }, + "location": { + "type": "string" + }, + "tagsAvailabilitySets": { + "type": "object" + } + }, + "resources": [ + { + "copy": { + "name": "availabilitySets", + "count": "[length(range(0, parameters('availabilitySetsCount')))]" + }, + "type": "Microsoft.Compute/availabilitySets", + "apiVersion": "2019-07-01", + "name": "[format('{0}{1}', parameters('availabilitySetNamePrefix'), padLeft(add(range(0, parameters('availabilitySetsCount'))[copyIndex()], parameters('availabilitySetsIndex')), 2, '0'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsAvailabilitySets')]", + "sku": { + "name": "Aligned" + }, + "properties": { + "platformUpdateDomainCount": 5, + "platformFaultDomainCount": 2 + } + } + ] + } + } + }, + { + "copy": { + "name": "roleAssignments", + "count": "[length(range(0, length(parameters('securityPrincipalObjectIds'))))]" + }, + "condition": "[not(contains(parameters('activeDirectorySolution'), 'DomainServices'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('RoleAssignments_{0}_{1}', range(0, length(parameters('securityPrincipalObjectIds')))[copyIndex()], parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupHosts')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "PrincipalId": { + "value": "[parameters('securityPrincipalObjectIds')[range(0, length(parameters('securityPrincipalObjectIds')))[copyIndex()]]]" + }, + "PrincipalType": { + "value": "Group" + }, + "RoleDefinitionId": { + "value": "[parameters('roleDefinitions').VirtualMachineUserLogin]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "3999918654980032531" + } + }, + "parameters": { + "PrincipalId": { + "type": "string" + }, + "PrincipalType": { + "type": "string" + }, + "RoleDefinitionId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('PrincipalId'), parameters('RoleDefinitionId'), resourceGroup().id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('RoleDefinitionId'))]", + "principalId": "[parameters('PrincipalId')]", + "principalType": "[parameters('PrincipalType')]" + } + } + ] + } + } + }, + { + "copy": { + "name": "virtualMachines", + "count": "[length(range(1, parameters('sessionHostBatchCount')))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('VirtualMachines_{0}_{1}', sub(range(1, parameters('sessionHostBatchCount'))[copyIndex()], 1), parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupHosts')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "acceleratedNetworking": { + "value": "[parameters('acceleratedNetworking')]" + }, + "activeDirectorySolution": { + "value": "[parameters('activeDirectorySolution')]" + }, + "artifactsUri": { + "value": "[parameters('artifactsUri')]" + }, + "artifactsUserAssignedIdentityClientId": { + "value": "[parameters('artifactsUserAssignedIdentityClientId')]" + }, + "artifactsUserAssignedIdentityResourceId": { + "value": "[parameters('artifactsUserAssignedIdentityResourceId')]" + }, + "availability": { + "value": "[parameters('availability')]" + }, + "availabilitySetNamePrefix": { + "value": "[parameters('availabilitySetNamePrefix')]" + }, + "availabilityZones": { + "value": "[parameters('availabilityZones')]" + }, + "avdAgentBootLoaderMsiName": { + "value": "[parameters('avdAgentBootLoaderMsiName')]" + }, + "avdAgentMsiName": { + "value": "[parameters('avdAgentMsiName')]" + }, + "batchCount": { + "value": "[range(1, parameters('sessionHostBatchCount'))[copyIndex()]]" + }, + "dataCollectionRuleAssociationName": { + "value": "[parameters('dataCollectionRuleAssociationName')]" + }, + "dataCollectionRuleResourceId": { + "value": "[parameters('dataCollectionRuleResourceId')]" + }, + "deploymentUserAssignedidentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + }, + "diskEncryptionSetResourceId": { + "value": "[parameters('diskEncryptionSetResourceId')]" + }, + "diskNamePrefix": { + "value": "[parameters('diskNamePrefix')]" + }, + "diskSku": { + "value": "[parameters('diskSku')]" + }, + "domainJoinPassword": { + "value": "[parameters('domainJoinPassword')]" + }, + "domainJoinUserPrincipalName": { + "value": "[parameters('domainJoinUserPrincipalName')]" + }, + "domainName": { + "value": "[parameters('domainName')]" + }, + "enableDrainMode": { + "value": "[parameters('drainMode')]" + }, + "fslogix": { + "value": "[parameters('fslogix')]" + }, + "fslogixContainerType": { + "value": "[parameters('fslogixContainerType')]" + }, + "hostPoolName": { + "value": "[parameters('hostPoolName')]" + }, + "hostPoolType": { + "value": "[parameters('hostPoolType')]" + }, + "imageDefinitionResourceId": { + "value": "[parameters('imageDefinitionResourceId')]" + }, + "imageOffer": { + "value": "[parameters('imageOffer')]" + }, + "imagePublisher": { + "value": "[parameters('imagePublisher')]" + }, + "imageSku": { + "value": "[parameters('imageSku')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "logAnalyticsWorkspaceName": { + "value": "[parameters('logAnalyticsWorkspaceName')]" + }, + "managementVirtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + }, + "monitoring": { + "value": "[parameters('monitoring')]" + }, + "netAppFileShares": { + "value": "[parameters('netAppFileShares')]" + }, + "networkInterfaceNamePrefix": { + "value": "[parameters('networkInterfaceNamePrefix')]" + }, + "organizationalUnitPath": { + "value": "[parameters('organizationalUnitPath')]" + }, + "resourceGroupControlPlane": { + "value": "[parameters('resourceGroupControlPlane')]" + }, + "resourceGroupManagement": { + "value": "[parameters('resourceGroupManagement')]" + }, + "securityLogAnalyticsWorkspaceResourceId": { + "value": "[parameters('securityLogAnalyticsWorkspaceResourceId')]" + }, + "sessionHostCount": "[if(and(equals(range(1, parameters('sessionHostBatchCount'))[copyIndex()], parameters('sessionHostBatchCount')), greater(parameters('divisionRemainderValue'), 0)), createObject('value', parameters('divisionRemainderValue')), createObject('value', parameters('maxResourcesPerTemplateDeployment')))]", + "sessionHostIndex": "[if(equals(range(1, parameters('sessionHostBatchCount'))[copyIndex()], 1), createObject('value', parameters('sessionHostIndex')), createObject('value', add(mul(sub(range(1, parameters('sessionHostBatchCount'))[copyIndex()], 1), parameters('maxResourcesPerTemplateDeployment')), parameters('sessionHostIndex'))))]", + "storageAccountPrefix": { + "value": "[parameters('storageAccountPrefix')]" + }, + "storageCount": { + "value": "[parameters('storageCount')]" + }, + "storageIndex": { + "value": "[parameters('storageIndex')]" + }, + "storageService": { + "value": "[parameters('storageService')]" + }, + "storageSuffix": { + "value": "[parameters('storageSuffix')]" + }, + "subnet": { + "value": "[parameters('subnet')]" + }, + "tagsNetworkInterfaces": { + "value": "[variables('tagsNetworkInterfaces')]" + }, + "tagsVirtualMachines": { + "value": "[variables('tagsVirtualMachines')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "virtualMachineMonitoringAgent": { + "value": "[parameters('virtualMachineMonitoringAgent')]" + }, + "virtualMachineNamePrefix": { + "value": "[parameters('virtualMachineNamePrefix')]" + }, + "virtualMachinePassword": { + "value": "[parameters('virtualMachinePassword')]" + }, + "virtualMachineSize": { + "value": "[parameters('virtualMachineSize')]" + }, + "virtualMachineUsername": { + "value": "[parameters('virtualMachineUsername')]" + }, + "virtualNetwork": { + "value": "[parameters('virtualNetwork')]" + }, + "virtualNetworkResourceGroup": { + "value": "[parameters('virtualNetworkResourceGroup')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "17511805322486017859" + } + }, + "parameters": { + "artifactsUri": { + "type": "string" + }, + "artifactsUserAssignedIdentityClientId": { + "type": "string" + }, + "artifactsUserAssignedIdentityResourceId": { + "type": "string" + }, + "acceleratedNetworking": { + "type": "string" + }, + "activeDirectorySolution": { + "type": "string" + }, + "availability": { + "type": "string" + }, + "availabilitySetNamePrefix": { + "type": "string" + }, + "availabilityZones": { + "type": "array" + }, + "avdAgentBootLoaderMsiName": { + "type": "string" + }, + "avdAgentMsiName": { + "type": "string" + }, + "batchCount": { + "type": "int" + }, + "dataCollectionRuleAssociationName": { + "type": "string" + }, + "dataCollectionRuleResourceId": { + "type": "string" + }, + "deploymentUserAssignedidentityClientId": { + "type": "string" + }, + "diskEncryptionSetResourceId": { + "type": "string" + }, + "diskNamePrefix": { + "type": "string" + }, + "diskSku": { + "type": "string" + }, + "domainJoinPassword": { + "type": "securestring" + }, + "domainJoinUserPrincipalName": { + "type": "string" + }, + "domainName": { + "type": "string" + }, + "enableDrainMode": { + "type": "bool" + }, + "fslogix": { + "type": "bool" + }, + "fslogixContainerType": { + "type": "string" + }, + "hostPoolName": { + "type": "string" + }, + "hostPoolType": { + "type": "string" + }, + "imageDefinitionResourceId": { + "type": "string" + }, + "imageOffer": { + "type": "string" + }, + "imagePublisher": { + "type": "string" + }, + "imageSku": { + "type": "string" + }, + "location": { + "type": "string" + }, + "logAnalyticsWorkspaceName": { + "type": "string" + }, + "managementVirtualMachineName": { + "type": "string" + }, + "monitoring": { + "type": "bool" + }, + "netAppFileShares": { + "type": "array" + }, + "networkInterfaceNamePrefix": { + "type": "string" + }, + "organizationalUnitPath": { + "type": "string" + }, + "resourceGroupControlPlane": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "securityLogAnalyticsWorkspaceResourceId": { + "type": "string" + }, + "sessionHostCount": { + "type": "int" + }, + "sessionHostIndex": { + "type": "int" + }, + "storageAccountPrefix": { + "type": "string" + }, + "storageCount": { + "type": "int" + }, + "storageIndex": { + "type": "int" + }, + "storageService": { + "type": "string" + }, + "storageSuffix": { + "type": "string" + }, + "subnet": { + "type": "string" + }, + "tagsNetworkInterfaces": { + "type": "object" + }, + "tagsVirtualMachines": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "virtualMachineMonitoringAgent": { + "type": "string" + }, + "virtualMachineNamePrefix": { + "type": "string" + }, + "virtualMachinePassword": { + "type": "securestring" + }, + "virtualMachineSize": { + "type": "string" + }, + "virtualMachineUsername": { + "type": "string" + }, + "virtualNetwork": { + "type": "string" + }, + "virtualNetworkResourceGroup": { + "type": "string" + } + }, + "variables": { + "amdVmSize": "[contains(variables('amdVmSizes'), parameters('virtualMachineSize'))]", + "amdVmSizes": [ + "Standard_NV4as_v4", + "Standard_NV8as_v4", + "Standard_NV16as_v4", + "Standard_NV32as_v4" + ], + "fslogixExclusions": "[format('\"%TEMP%\\*\\*.VHDX\";\"%Windir%\\TEMP\\*\\*.VHDX\"{0}{1}{2}', variables('fslogixExclusionsCloudCache'), variables('fslogixExclusionsProfileContainers'), variables('fslogixExclusionsOfficeContainers'))]", + "fslogixExclusionsCloudCache": "[if(contains(parameters('fslogixContainerType'), 'CloudCache'), ';\"%ProgramData%\\fslogix\\Cache\\*\";\"%ProgramData%\\fslogix\\Proxy\\*\"', '')]", + "fslogixExclusionsOfficeContainers": "[if(contains(parameters('fslogixContainerType'), 'Office'), format(';\"{0}\";\"{1}.lock\";\"{2}.meta\";\"{3}.metadata\"', variables('fslogixOfficeShare'), variables('fslogixOfficeShare'), variables('fslogixOfficeShare'), variables('fslogixOfficeShare')), '')]", + "fslogixExclusionsProfileContainers": "[format(';\"{0}\";\"{1}.lock\";\"{2}.meta\";\"{3}.metadata\"', variables('fslogixProfileShare'), variables('fslogixProfileShare'), variables('fslogixProfileShare'), variables('fslogixProfileShare'))]", + "fslogixOfficeShare": "[format('\\\\{0}??.file.{1}\\office-containers\\*\\*.VHDX', parameters('storageAccountPrefix'), parameters('storageSuffix'))]", + "fslogixProfileShare": "[format('\\\\{0}??.file.{1}\\profile-containers\\*\\*.VHDX', parameters('storageAccountPrefix'), parameters('storageSuffix'))]", + "imageReference": "[if(empty(parameters('imageDefinitionResourceId')), createObject('publisher', parameters('imagePublisher'), 'offer', parameters('imageOffer'), 'sku', parameters('imageSku'), 'version', 'latest'), createObject('id', parameters('imageDefinitionResourceId')))]", + "intune": "[contains(parameters('activeDirectorySolution'), 'intuneEnrollment')]", + "nvidiaVmSize": "[contains(variables('nvidiaVmSizes'), parameters('virtualMachineSize'))]", + "nvidiaVmSizes": [ + "Standard_NV6", + "Standard_NV12", + "Standard_NV24", + "Standard_NV12s_v3", + "Standard_NV24s_v3", + "Standard_NV48s_v3", + "Standard_NC4as_T4_v3", + "Standard_NC8as_T4_v3", + "Standard_NC16as_T4_v3", + "Standard_NC64as_T4_v3", + "Standard_NV6ads_A10_v5", + "Standard_NV12ads_A10_v5", + "Standard_NV18ads_A10_v5", + "Standard_NV36ads_A10_v5", + "Standard_NV36adms_A10_v5", + "Standard_NV72ads_A10_v5" + ], + "pooledHostPool": "[equals(split(parameters('hostPoolType'), ' ')[0], 'Pooled')]", + "securitylogAnalyticsWorkspaceName": "[if(variables('securityMonitoring'), split(parameters('securityLogAnalyticsWorkspaceResourceId'), '/')[8], '')]", + "securityLogAnalyticsWorkspaceResourceGroupName": "[if(variables('securityMonitoring'), split(parameters('securityLogAnalyticsWorkspaceResourceId'), '/')[4], resourceGroup().name)]", + "securityLogAnalyticsWorkspaceSubscriptionId": "[if(variables('securityMonitoring'), split(parameters('securityLogAnalyticsWorkspaceResourceId'), '/')[2], subscription().subscriptionId)]", + "securityMonitoring": "[if(empty(parameters('securityLogAnalyticsWorkspaceResourceId')), false(), true())]" + }, + "resources": [ + { + "copy": { + "name": "networkInterface", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2020-05-01", + "name": "[format('{0}{1}', parameters('networkInterfaceNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[copyIndex()], parameters('sessionHostIndex')), 4, '0'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsNetworkInterfaces')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[resourceId(subscription().subscriptionId, parameters('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetwork'), parameters('subnet'))]" + }, + "primary": true, + "privateIPAddressVersion": "IPv4" + } + } + ], + "enableAcceleratedNetworking": "[if(equals(parameters('acceleratedNetworking'), 'True'), true(), false())]", + "enableIPForwarding": false + } + }, + { + "copy": { + "name": "virtualMachine", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2021-03-01", + "name": "[format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[copyIndex()], parameters('sessionHostIndex')), 4, '0'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "zones": "[if(equals(parameters('availability'), 'AvailabilityZones'), createArray(parameters('availabilityZones')[mod(range(0, parameters('sessionHostCount'))[copyIndex()], length(parameters('availabilityZones')))]), null())]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', parameters('artifactsUserAssignedIdentityResourceId'))]": {} + } + }, + "properties": { + "availabilitySet": "[if(equals(parameters('availability'), 'AvailabilitySets'), createObject('id', resourceId('Microsoft.Compute/availabilitySets', format('{0}{1}', parameters('availabilitySetNamePrefix'), padLeft(div(add(range(0, parameters('sessionHostCount'))[copyIndex()], parameters('sessionHostIndex')), 200), 2, '0')))), null())]", + "hardwareProfile": { + "vmSize": "[parameters('virtualMachineSize')]" + }, + "storageProfile": { + "imageReference": "[variables('imageReference')]", + "osDisk": { + "name": "[format('{0}{1}', parameters('diskNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[copyIndex()], parameters('sessionHostIndex')), 4, '0'))]", + "osType": "Windows", + "createOption": "FromImage", + "caching": "ReadWrite", + "deleteOption": "Delete", + "managedDisk": { + "diskEncryptionSet": { + "id": "[parameters('diskEncryptionSetResourceId')]" + }, + "storageAccountType": "[parameters('diskSku')]" + } + }, + "dataDisks": [] + }, + "osProfile": { + "computerName": "[format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[copyIndex()], parameters('sessionHostIndex')), 4, '0'))]", + "adminUsername": "[parameters('virtualMachineUsername')]", + "adminPassword": "[parameters('virtualMachinePassword')]", + "windowsConfiguration": { + "provisionVMAgent": true, + "enableAutomaticUpdates": false + }, + "secrets": [], + "allowExtensionOperations": true + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', format('{0}{1}', parameters('networkInterfaceNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[copyIndex()], parameters('sessionHostIndex')), 4, '0')))]", + "properties": { + "deleteOption": "Delete" + } + } + ] + }, + "securityProfile": { + "uefiSettings": { + "secureBootEnabled": true, + "vTpmEnabled": true + }, + "securityType": "trustedLaunch", + "encryptionAtHost": true + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "licenseType": "[if(equals(parameters('imagePublisher'), 'MicrosoftWindowsDesktop'), 'Windows_Client', 'Windows_Server')]" + }, + "dependsOn": [ + "networkInterface" + ] + }, + { + "copy": { + "name": "extension_IaasAntimalware", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')), 'IaaSAntimalware')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "publisher": "Microsoft.Azure.Security", + "type": "IaaSAntimalware", + "typeHandlerVersion": "1.3", + "autoUpgradeMinorVersion": true, + "enableAutomaticUpgrade": false, + "settings": { + "AntimalwareEnabled": true, + "RealtimeProtectionEnabled": "true", + "ScheduledScanSettings": { + "isEnabled": "true", + "day": "7", + "time": "120", + "scanType": "Quick" + }, + "Exclusions": "[if(parameters('fslogix'), createObject('Paths', variables('fslogixExclusions')), createObject())]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]" + ] + }, + { + "copy": { + "name": "extension_GuestAttestation", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')), 'GuestAttestation')]", + "location": "[parameters('location')]", + "properties": { + "publisher": "Microsoft.Azure.Security.WindowsAttestation", + "type": "GuestAttestation", + "typeHandlerVersion": "1.0", + "autoUpgradeMinorVersion": true, + "settings": { + "AttestationConfig": { + "MaaSettings": { + "maaEndpoint": "", + "maaTenantName": "GuestAttestation" + }, + "AscSettings": { + "ascReportingEndpoint": "", + "ascReportingFrequency": "" + }, + "useCustomToken": "false", + "disableAlerts": "false" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]" + ] + }, + { + "copy": { + "name": "extension_MicrosoftMonitoringAgent", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "condition": "[and(parameters('monitoring'), equals(parameters('virtualMachineMonitoringAgent'), 'LogAnalyticsAgent'))]", + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')), 'MicrosoftmonitoringAgent')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "publisher": "Microsoft.EnterpriseCloud.monitoring", + "type": "MicrosoftmonitoringAgent", + "typeHandlerVersion": "1.0", + "autoUpgradeMinorVersion": true, + "settings": { + "workspaceId": "[if(parameters('monitoring'), reference(resourceId(parameters('resourceGroupManagement'), 'Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceName')), '2015-03-20').customerId, null())]" + }, + "protectedSettings": { + "workspaceKey": "[if(parameters('monitoring'), listKeys(resourceId(parameters('resourceGroupManagement'), 'Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceName')), '2015-03-20').primarySharedKey, null())]" + } + }, + "dependsOn": [ + "extension_IaasAntimalware", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]" + ] + }, + { + "copy": { + "name": "extension_AzureMonitorWindowsAgent", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "condition": "[and(parameters('monitoring'), equals(parameters('virtualMachineMonitoringAgent'), 'AzureMonitorAgent'))]", + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2023-03-01", + "name": "[format('{0}/{1}', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')), 'AzureMonitorWindowsAgent')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "publisher": "Microsoft.Azure.Monitor", + "type": "AzureMonitorWindowsAgent", + "typeHandlerVersion": "1.0", + "autoUpgradeMinorVersion": true, + "enableAutomaticUpgrade": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]" + ] + }, + { + "copy": { + "name": "dataCollectionRuleAssociation", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "condition": "[and(parameters('monitoring'), equals(parameters('virtualMachineMonitoringAgent'), 'AzureMonitorAgent'))]", + "type": "Microsoft.Insights/dataCollectionRuleAssociations", + "apiVersion": "2022-06-01", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]", + "name": "[parameters('dataCollectionRuleAssociationName')]", + "properties": { + "dataCollectionRuleId": "[parameters('dataCollectionRuleResourceId')]", + "description": "AVD Insights data collection rule association" + }, + "dependsOn": [ + "extension_AzureMonitorWindowsAgent", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]" + ] + }, + { + "copy": { + "name": "extension_CustomScriptExtension", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')), 'CustomScriptExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "fileUris": [ + "[format('{0}{1}', parameters('artifactsUri'), parameters('avdAgentBootLoaderMsiName'))]", + "[format('{0}{1}', parameters('artifactsUri'), parameters('avdAgentMsiName'))]", + "[format('{0}Set-SessionHostConfiguration.ps1', parameters('artifactsUri'))]" + ], + "timestamp": "[parameters('timestamp')]" + }, + "protectedSettings": { + "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File Set-SessionHostConfiguration.ps1 -activeDirectorySolution {0} -amdVmSize {1} -avdAgentBootLoaderMsiName \"{2}\" -avdAgentMsiName \"{3}\" -Environment {4} -fslogix {5} -fslogixContainerType {6} -hostPoolName {7} -HostPoolRegistrationToken \"{8}\" -imageOffer {9} -imagePublisher {10} -netAppFileShares {11} -nvidiaVmSize {12} -pooledHostPool {13} -securityMonitoring {14} -SecurityWorkspaceId {15} -securityWorkspaceKey \"{16}\" -storageAccountPrefix {17} -storageCount {18} -storageIndex {19} -storageService {20} -storageSuffix {21}', parameters('activeDirectorySolution'), variables('amdVmSize'), parameters('avdAgentBootLoaderMsiName'), parameters('avdAgentMsiName'), environment().name, parameters('fslogix'), parameters('fslogixContainerType'), parameters('hostPoolName'), reference(resourceId(parameters('resourceGroupControlPlane'), 'Microsoft.DesktopVirtualization/hostpools', parameters('hostPoolName')), '2019-12-10-preview').registrationInfo.token, parameters('imageOffer'), parameters('imagePublisher'), parameters('netAppFileShares'), variables('nvidiaVmSize'), variables('pooledHostPool'), variables('securityMonitoring'), if(variables('securityMonitoring'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('securityLogAnalyticsWorkspaceSubscriptionId'), variables('securityLogAnalyticsWorkspaceResourceGroupName')), 'Microsoft.OperationalInsights/workspaces', variables('securitylogAnalyticsWorkspaceName')), '2021-06-01').customerId, 'NotApplicable'), if(variables('securityMonitoring'), listKeys(parameters('securityLogAnalyticsWorkspaceResourceId'), '2021-06-01').primarySharedKey, 'NotApplicable'), parameters('storageAccountPrefix'), parameters('storageCount'), parameters('storageIndex'), parameters('storageService'), parameters('storageSuffix'))]", + "managedidentity": { + "clientId": "[parameters('artifactsUserAssignedIdentityClientId')]" + } + } + }, + "dependsOn": [ + "dataCollectionRuleAssociation", + "extension_MicrosoftMonitoringAgent", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]" + ] + }, + { + "copy": { + "name": "extension_JsonADDomainExtension", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "condition": "[contains(parameters('activeDirectorySolution'), 'DomainServices')]", + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')), 'JsonADDomainExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "forceUpdateTag": "[parameters('timestamp')]", + "publisher": "Microsoft.Compute", + "type": "JsonADDomainExtension", + "typeHandlerVersion": "1.3", + "autoUpgradeMinorVersion": true, + "settings": { + "Name": "[parameters('domainName')]", + "Options": "3", + "OUPath": "[parameters('organizationalUnitPath')]", + "Restart": "true", + "User": "[parameters('domainJoinUserPrincipalName')]" + }, + "protectedSettings": { + "Password": "[parameters('domainJoinPassword')]" + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('CSE_DrainMode_{0}_{1}', parameters('batchCount'), parameters('timestamp')))]", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]" + ] + }, + { + "copy": { + "name": "extension_AADLoginForWindows", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "condition": "[not(contains(parameters('activeDirectorySolution'), 'DomainServices'))]", + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')), 'AADLoginForWindows')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "publisher": "Microsoft.Azure.ActiveDirectory", + "type": "AADLoginForWindows", + "typeHandlerVersion": "2.0", + "autoUpgradeMinorVersion": true, + "settings": "[if(variables('intune'), createObject('mdmId', '0000000a-0000-0000-c000-000000000000'), null())]" + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('CSE_DrainMode_{0}_{1}', parameters('batchCount'), parameters('timestamp')))]", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]" + ] + }, + { + "copy": { + "name": "extension_AmdGpuDriverWindows", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "condition": "[variables('amdVmSize')]", + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')), 'AmdGpuDriverWindows')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "publisher": "Microsoft.HpcCompute", + "type": "AmdGpuDriverWindows", + "typeHandlerVersion": "1.0", + "autoUpgradeMinorVersion": true, + "settings": {} + }, + "dependsOn": [ + "extension_AADLoginForWindows", + "extension_JsonADDomainExtension", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]" + ] + }, + { + "copy": { + "name": "extension_NvidiaGpuDriverWindows", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "condition": "[variables('nvidiaVmSize')]", + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')), 'NvidiaGpuDriverWindows')]", + "location": "[parameters('location')]", + "tags": "[parameters('tagsVirtualMachines')]", + "properties": { + "publisher": "Microsoft.HpcCompute", + "type": "NvidiaGpuDriverWindows", + "typeHandlerVersion": "1.2", + "autoUpgradeMinorVersion": true, + "settings": {} + }, + "dependsOn": [ + "extension_AADLoginForWindows", + "extension_JsonADDomainExtension", + "[resourceId('Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[range(0, parameters('sessionHostCount'))[copyIndex()]], parameters('sessionHostIndex')), 4, '0')))]" + ] + }, + { + "condition": "[parameters('enableDrainMode')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('CSE_DrainMode_{0}_{1}', parameters('batchCount'), parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileUris": { + "value": [ + "[format('{0}Set-AvdDrainMode.ps1', parameters('artifactsUri'))]" + ] + }, + "location": { + "value": "[parameters('location')]" + }, + "parameters": { + "value": "[format('-Environment {0} -hostPoolName {1} -HostPoolResourceGroupName {2} -sessionHostCount {3} -sessionHostIndex {4} -SubscriptionId {5} -TenantId {6} -userAssignedidentityClientId {7} -virtualMachineNamePrefix {8}', environment().name, parameters('hostPoolName'), parameters('resourceGroupControlPlane'), parameters('sessionHostCount'), parameters('sessionHostIndex'), subscription().subscriptionId, tenant().tenantId, parameters('deploymentUserAssignedidentityClientId'), parameters('virtualMachineNamePrefix'))]" + }, + "scriptFileName": { + "value": "Set-AvdDrainMode.ps1" + }, + "tags": { + "value": "[parameters('tagsVirtualMachines')]" + }, + "userAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedidentityClientId')]" + }, + "virtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12496195847910288876" + } + }, + "parameters": { + "fileUris": { + "type": "array" + }, + "location": { + "type": "string" + }, + "parameters": { + "type": "securestring" + }, + "scriptFileName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]" + }, + "userAssignedIdentityClientId": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), 'CustomScriptExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "timestamp": "[parameters('timestamp')]" + }, + "protectedSettings": { + "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File {0} {1}', parameters('scriptFileName'), parameters('parameters'))]", + "fileUris": "[parameters('fileUris')]", + "managedIdentity": { + "clientId": "[parameters('userAssignedIdentityClientId')]" + } + } + } + } + ], + "outputs": { + "value": { + "type": "object", + "value": "[json(filter(reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'CustomScriptExtension'), '2021-03-01').instanceView.substatuses, lambda('item', equals(lambdaVariables('item').code, 'ComponentStatus/StdOut/succeeded')))[0].message)]" + } + } + } + }, + "dependsOn": [ + "extension_CustomScriptExtension" + ] + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupHosts')), 'Microsoft.Resources/deployments', format('availabilitySets_{0}', parameters('timestamp')))]" + ] + }, + { + "condition": "[and(parameters('enableRecoveryServices'), contains(parameters('hostPoolType'), 'Personal'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('RecoveryServices_VirtualMachines_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "divisionRemainderValue": { + "value": "[parameters('divisionRemainderValue')]" + }, + "fslogix": { + "value": "[parameters('fslogix')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "maxResourcesPerTemplateDeployment": { + "value": "[parameters('maxResourcesPerTemplateDeployment')]" + }, + "recoveryServicesVaultName": { + "value": "[parameters('recoveryServicesVaultName')]" + }, + "resourceGroupHosts": { + "value": "[parameters('resourceGroupHosts')]" + }, + "resourceGroupManagement": { + "value": "[parameters('resourceGroupManagement')]" + }, + "sessionHostBatchCount": { + "value": "[parameters('sessionHostBatchCount')]" + }, + "sessionHostIndex": { + "value": "[parameters('sessionHostIndex')]" + }, + "tagsRecoveryServicesVault": { + "value": "[variables('tagsRecoveryServicesVault')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "virtualMachineNamePrefix": { + "value": "[parameters('virtualMachineNamePrefix')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "13734051951512369688" + } + }, + "parameters": { + "divisionRemainderValue": { + "type": "int" + }, + "fslogix": { + "type": "bool" + }, + "location": { + "type": "string" + }, + "maxResourcesPerTemplateDeployment": { + "type": "int" + }, + "recoveryServicesVaultName": { + "type": "string" + }, + "resourceGroupHosts": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "sessionHostBatchCount": { + "type": "int" + }, + "sessionHostIndex": { + "type": "int" + }, + "tagsRecoveryServicesVault": { + "type": "object" + }, + "timestamp": { + "type": "string" + }, + "virtualMachineNamePrefix": { + "type": "string" + } + }, + "resources": [ + { + "copy": { + "name": "protectedItems_Vm", + "count": "[length(range(1, parameters('sessionHostBatchCount')))]" + }, + "condition": "[not(parameters('fslogix'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('BackupProtectedItems_VirtualMachines_{0}_{1}', sub(range(1, parameters('sessionHostBatchCount'))[copyIndex()], 1), parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "policyId": { + "value": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.RecoveryServices/vaults/backupPolicies', parameters('recoveryServicesVaultName'), 'AvdPolicyVm')]" + }, + "recoveryServicesVaultName": { + "value": "[parameters('recoveryServicesVaultName')]" + }, + "sessionHostCount": "[if(and(equals(range(1, parameters('sessionHostBatchCount'))[copyIndex()], parameters('sessionHostBatchCount')), greater(parameters('divisionRemainderValue'), 0)), createObject('value', parameters('divisionRemainderValue')), createObject('value', parameters('maxResourcesPerTemplateDeployment')))]", + "sessionHostIndex": "[if(equals(range(1, parameters('sessionHostBatchCount'))[copyIndex()], 1), createObject('value', parameters('sessionHostIndex')), createObject('value', add(mul(sub(range(1, parameters('sessionHostBatchCount'))[copyIndex()], 1), parameters('maxResourcesPerTemplateDeployment')), parameters('sessionHostIndex'))))]", + "tags": { + "value": "[parameters('tagsRecoveryServicesVault')]" + }, + "virtualMachineNamePrefix": { + "value": "[parameters('virtualMachineNamePrefix')]" + }, + "virtualMachineResourceGroupName": { + "value": "[parameters('resourceGroupHosts')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "13602826513485540699" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "policyId": { + "type": "string" + }, + "recoveryServicesVaultName": { + "type": "string" + }, + "sessionHostCount": { + "type": "int" + }, + "sessionHostIndex": { + "type": "int" + }, + "tags": { + "type": "object" + }, + "virtualMachineNamePrefix": { + "type": "string" + }, + "virtualMachineResourceGroupName": { + "type": "string" + } + }, + "variables": { + "v2VmContainer": "iaasvmcontainer;iaasvmcontainerv2;", + "v2Vm": "vm;iaasvmcontainerv2;" + }, + "resources": [ + { + "copy": { + "name": "protectedItems_Vm", + "count": "[length(range(0, parameters('sessionHostCount')))]" + }, + "type": "Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems", + "apiVersion": "2021-08-01", + "name": "[format('{0}/Azure/{1}{2};{3}{4}/{5}{6};{7}{8}', parameters('recoveryServicesVaultName'), variables('v2VmContainer'), parameters('virtualMachineResourceGroupName'), parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[copyIndex()], parameters('sessionHostIndex')), 4, '0'), variables('v2Vm'), parameters('virtualMachineResourceGroupName'), parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[copyIndex()], parameters('sessionHostIndex')), 4, '0'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "protectedItemType": "Microsoft.Compute/virtualMachines", + "policyId": "[parameters('policyId')]", + "sourceResourceId": "[resourceId(parameters('virtualMachineResourceGroupName'), 'Microsoft.Compute/virtualMachines', format('{0}{1}', parameters('virtualMachineNamePrefix'), padLeft(add(range(0, parameters('sessionHostCount'))[copyIndex()], parameters('sessionHostIndex')), 4, '0')))]" + } + } + ] + } + } + } + ] + } + }, + "dependsOn": [ + "virtualMachines" + ] + }, + { + "condition": "[and(parameters('enableScalingTool'), parameters('pooledHostPool'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('ScalingTool_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "artifactsUri": { + "value": "[parameters('artifactsUri')]" + }, + "automationAccountName": { + "value": "[parameters('automationAccountName')]" + }, + "beginPeakTime": { + "value": "[parameters('scalingBeginPeakTime')]" + }, + "endPeakTime": { + "value": "[parameters('scalingEndPeakTime')]" + }, + "hostPoolName": { + "value": "[parameters('hostPoolName')]" + }, + "hostPoolResourceGroupName": { + "value": "[parameters('resourceGroupControlPlane')]" + }, + "hybridRunbookWorkerGroupName": { + "value": "[parameters('hybridRunbookWorkerGroupName')]" + }, + "limitSecondsToForceLogOffUser": { + "value": "[parameters('scalingLimitSecondsToForceLogOffUser')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "managementVirtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + }, + "minimumNumberOfRdsh": { + "value": "[parameters('scalingMinimumNumberOfRdsh')]" + }, + "resourceGroupControlPlane": { + "value": "[parameters('resourceGroupControlPlane')]" + }, + "resourceGroupHosts": { + "value": "[parameters('resourceGroupHosts')]" + }, + "sessionThresholdPerCPU": { + "value": "[parameters('scalingSessionThresholdPerCPU')]" + }, + "tags": { + "value": "[variables('tagsAutomationAccounts')]" + }, + "timeDifference": { + "value": "[parameters('timeDifference')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "timeZone": { + "value": "[parameters('timeZone')]" + }, + "userAssignedIdentityClientId": { + "value": "[parameters('deploymentUserAssignedIdentityClientId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "656881460375596525" + } + }, + "parameters": { + "artifactsUri": { + "type": "string" + }, + "automationAccountName": { + "type": "string" + }, + "beginPeakTime": { + "type": "string" + }, + "endPeakTime": { + "type": "string" + }, + "hostPoolName": { + "type": "string" + }, + "hostPoolResourceGroupName": { + "type": "string" + }, + "hybridRunbookWorkerGroupName": { + "type": "string" + }, + "limitSecondsToForceLogOffUser": { + "type": "string" + }, + "location": { + "type": "string" + }, + "managementVirtualMachineName": { + "type": "string" + }, + "minimumNumberOfRdsh": { + "type": "string" + }, + "resourceGroupControlPlane": { + "type": "string" + }, + "resourceGroupHosts": { + "type": "string" + }, + "sessionThresholdPerCPU": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timeDifference": { + "type": "string" + }, + "time": { + "type": "string", + "defaultValue": "[utcNow('u')]" + }, + "timestamp": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "userAssignedIdentityClientId": { + "type": "string" + } + }, + "variables": { + "roleAssignments": [ + "[parameters('resourceGroupControlPlane')]", + "[parameters('resourceGroupHosts')]" + ], + "runbookFileName": "Set-HostPoolScaling.ps1", + "scriptFileName": "Set-AutomationRunbook.ps1" + }, + "resources": [ + { + "copy": { + "name": "schedules", + "count": "[length(range(0, 4))]" + }, + "type": "Microsoft.Automation/automationAccounts/schedules", + "apiVersion": "2022-08-08", + "name": "[format('{0}/{1}', parameters('automationAccountName'), format('{0}_{1}min', parameters('hostPoolName'), mul(add(range(0, 4)[copyIndex()], 1), 15)))]", + "properties": { + "advancedSchedule": {}, + "description": null, + "expiryTime": null, + "frequency": "Hour", + "interval": 1, + "startTime": "[dateTimeAdd(parameters('time'), format('PT{0}M', mul(add(range(0, 4)[copyIndex()], 1), 15)))]", + "timeZone": "[parameters('timeZone')]" + } + }, + { + "copy": { + "name": "jobSchedules", + "count": "[length(range(0, 4))]" + }, + "type": "Microsoft.Automation/automationAccounts/jobSchedules", + "apiVersion": "2022-08-08", + "name": "[format('{0}/{1}', parameters('automationAccountName'), guid(parameters('time'), variables('runbookFileName'), parameters('hostPoolName'), string(range(0, 4)[copyIndex()])))]", + "properties": { + "parameters": { + "beginPeakTime": "[parameters('beginPeakTime')]", + "endPeakTime": "[parameters('endPeakTime')]", + "EnvironmentName": "[environment().name]", + "hostPoolName": "[parameters('hostPoolName')]", + "limitSecondsToForceLogOffUser": "[parameters('limitSecondsToForceLogOffUser')]", + "LogOffMessageBody": "Your session will be logged off. Please save and close everything.", + "LogOffMessageTitle": "Machine is about to shutdown.", + "MaintenanceTagName": "Maintenance", + "minimumNumberOfRdsh": "[parameters('minimumNumberOfRdsh')]", + "ResourceGroupName": "[parameters('hostPoolResourceGroupName')]", + "sessionThresholdPerCPU": "[parameters('sessionThresholdPerCPU')]", + "SubscriptionId": "[subscription().subscriptionId]", + "TenantId": "[subscription().tenantId]", + "timeDifference": "[parameters('timeDifference')]" + }, + "runbook": { + "name": "[replace(variables('runbookFileName'), '.ps1', '')]" + }, + "runOn": "[parameters('hybridRunbookWorkerGroupName')]", + "schedule": { + "name": "[format('{0}_{1}min', parameters('hostPoolName'), mul(add(range(0, 4)[range(0, 4)[copyIndex()]], 1), 15))]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('Runbook_{0}', parameters('timestamp')))]", + "[resourceId('Microsoft.Automation/automationAccounts/schedules', parameters('automationAccountName'), format('{0}_{1}min', parameters('hostPoolName'), mul(add(range(0, 4)[range(0, 4)[copyIndex()]], 1), 15)))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('Runbook_{0}', parameters('timestamp'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fileUris": { + "value": [ + "[format('{0}{1}', parameters('artifactsUri'), variables('runbookFileName'))]", + "[format('{0}{1}', parameters('artifactsUri'), variables('scriptFileName'))]" + ] + }, + "location": { + "value": "[parameters('location')]" + }, + "parameters": { + "value": "[format('-AutomationAccountName {0} -Environment {1} -ResourceGroupName {2} -RunbookFileName {3} -SubscriptionId {4} -TenantId {5} -UserAssignedIdentityClientId {6}', parameters('automationAccountName'), environment().name, resourceGroup().name, variables('runbookFileName'), subscription().subscriptionId, tenant().tenantId, parameters('userAssignedIdentityClientId'))]" + }, + "scriptFileName": { + "value": "[variables('scriptFileName')]" + }, + "tags": "[if(contains(parameters('tags'), 'Microsoft.Compute/virtualMachines'), createObject('value', parameters('tags')['Microsoft.Compute/virtualMachines']), createObject('value', createObject()))]", + "userAssignedIdentityClientId": { + "value": "[parameters('userAssignedIdentityClientId')]" + }, + "virtualMachineName": { + "value": "[parameters('managementVirtualMachineName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "12496195847910288876" + } + }, + "parameters": { + "fileUris": { + "type": "array" + }, + "location": { + "type": "string" + }, + "parameters": { + "type": "securestring" + }, + "scriptFileName": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "timestamp": { + "type": "string", + "defaultValue": "[utcNow('yyyyMMddhhmmss')]" + }, + "userAssignedIdentityClientId": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), 'CustomScriptExtension')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "timestamp": "[parameters('timestamp')]" + }, + "protectedSettings": { + "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File {0} {1}', parameters('scriptFileName'), parameters('parameters'))]", + "fileUris": "[parameters('fileUris')]", + "managedIdentity": { + "clientId": "[parameters('userAssignedIdentityClientId')]" + } + } + } + } + ], + "outputs": { + "value": { + "type": "object", + "value": "[json(filter(reference(resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'CustomScriptExtension'), '2021-03-01').instanceView.substatuses, lambda('item', equals(lambdaVariables('item').code, 'ComponentStatus/StdOut/succeeded')))[0].message)]" + } + } + } + } + }, + { + "copy": { + "name": "roleAssignment", + "count": "[length(range(0, length(variables('roleAssignments'))))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('RoleAssignment_{0}_{1}', range(0, length(variables('roleAssignments')))[copyIndex()], variables('roleAssignments')[range(0, length(variables('roleAssignments')))[copyIndex()]])]", + "resourceGroup": "[variables('roleAssignments')[range(0, length(variables('roleAssignments')))[copyIndex()]]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "PrincipalId": { + "value": "[reference(resourceId('Microsoft.Automation/automationAccounts', parameters('automationAccountName')), '2022-08-08', 'full').identity.principalId]" + }, + "PrincipalType": { + "value": "ServicePrincipal" + }, + "RoleDefinitionId": { + "value": "40c5ff49-9181-41f8-ae61-143b0e78555e" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "3999918654980032531" + } + }, + "parameters": { + "PrincipalId": { + "type": "string" + }, + "PrincipalType": { + "type": "string" + }, + "RoleDefinitionId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('PrincipalId'), parameters('RoleDefinitionId'), resourceGroup().id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('RoleDefinitionId'))]", + "principalId": "[parameters('PrincipalId')]", + "principalType": "[parameters('PrincipalType')]" + } + } + ] + } + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupManagement')), 'Microsoft.Resources/deployments', format('RecoveryServices_VirtualMachines_{0}', parameters('timestamp')))]" + ] + } + ] + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/deployments', format('FSLogix_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Logic_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Network_ControlPlane_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Network_Hosts_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp')))]", + "rgs" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('CleanUp_{0}', parameters('timestamp'))]", + "location": "[deployment().location]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "fslogixStorageService": { + "value": "[parameters('fslogixStorageService')]" + }, + "location": { + "value": "[parameters('locationVirtualMachines')]" + }, + "resourceGroupManagement": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp'))), '2022-09-01').outputs.resourceGroupManagement.value]" + }, + "scalingTool": { + "value": "[parameters('scalingTool')]" + }, + "timestamp": { + "value": "[parameters('timestamp')]" + }, + "userAssignedIdentityClientId": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.deploymentUserAssignedIdentityClientId.value]" + }, + "virtualMachineName": { + "value": "[reference(subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp'))), '2022-09-01').outputs.virtualMachineName.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "4860825122605188209" + } + }, + "parameters": { + "fslogixStorageService": { + "type": "string" + }, + "location": { + "type": "string" + }, + "resourceGroupManagement": { + "type": "string" + }, + "scalingTool": { + "type": "bool" + }, + "timestamp": { + "type": "string" + }, + "userAssignedIdentityClientId": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "condition": "[and(not(parameters('scalingTool')), not(equals(parameters('fslogixStorageService'), 'AzureFiles Premium')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('RemoveManagementVirtualMachine_{0}', parameters('timestamp'))]", + "resourceGroup": "[parameters('resourceGroupManagement')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "Location": { + "value": "[parameters('location')]" + }, + "UserAssignedIdentityClientId": { + "value": "[parameters('userAssignedIdentityClientId')]" + }, + "VirtualMachineName": { + "value": "[parameters('virtualMachineName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.3.34343", + "templateHash": "4526223439767657363" + } + }, + "parameters": { + "Location": { + "type": "string" + }, + "UserAssignedIdentityClientId": { + "type": "string" + }, + "VirtualMachineName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines/runCommands", + "apiVersion": "2023-03-01", + "name": "[format('{0}/{1}', parameters('VirtualMachineName'), 'RunCommand')]", + "location": "[parameters('Location')]", + "properties": { + "treatFailureAsDeploymentFailure": true, + "asyncExecution": true, + "parameters": [ + { + "name": "Environment", + "value": "[environment().name]" + }, + { + "name": "ResourceGroupName", + "value": "[resourceGroup().name]" + }, + { + "name": "SubscriptionId", + "value": "[subscription().subscriptionId]" + }, + { + "name": "TenantId", + "value": "[tenant().tenantId]" + }, + { + "name": "UserAssignedIdentityClientId", + "value": "[parameters('UserAssignedIdentityClientId')]" + }, + { + "name": "VirtualMachineName", + "value": "[parameters('VirtualMachineName')]" + } + ], + "source": { + "script": " param(\n [string]$Environment,\n [string]$ResourceGroupName,\n [string]$SubscriptionId,\n [string]$TenantId,\n [string]$UserAssignedIdentityClientId,\n [string]$VirtualMachineName\n )\n Start-Sleep -Seconds 30\n Connect-AzAccount -Environment $Environment -Tenant $TenantId -Subscription $SubscriptionId -Identity -AccountId $UserAssignedIdentityClientId\n Remove-AzVM -ResourceGroupName $ResourceGroupName -Name $VirtualMachineName -NoWait -Force\n " + } + } + } + ] + } + } + } + ] + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/deployments', format('FSLogix_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('Management_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('ResourceNames_{0}', parameters('timestamp')))]", + "[subscriptionResourceId('Microsoft.Resources/deployments', format('SessionHosts_{0}', parameters('timestamp')))]" + ] + } + ] +} \ No newline at end of file diff --git a/src/bicep/add-ons/azureVirtualDesktop/uiDefinition.json b/src/bicep/add-ons/azureVirtualDesktop/uiDefinition.json new file mode 100644 index 000000000..4149f063d --- /dev/null +++ b/src/bicep/add-ons/azureVirtualDesktop/uiDefinition.json @@ -0,0 +1,1612 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2021-09-09/uiFormDefinition.schema.json", + "view": { + "kind": "Form", + "properties": { + "title": "Mission Landing Zone Add-On: Azure Virtual Desktop", + "steps": [ + { + "name": "basics", + "label": "Basics", + "elements": [ + { + "name": "description", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "The Azure Virtual Desktop (AVD) add-on reduces the complexity in deploying AVD with SCCA and zero trust compliance. Click on the link below to learn more about the solution.", + "link": { + "label": "https://github.com/jamasten/AzureVirtualDesktop/blob/main/README.md", + "uri": "https://github.com/jamasten/AzureVirtualDesktop/blob/main/README.md" + } + } + }, + { + "name": "scope", + "type": "Microsoft.Common.ResourceScope", + "instanceDetailsLabel": "", + "location": { + "resourceTypes": [] + } + }, + { + "name": "hub", + "label": "Hub Resources", + "type": "Microsoft.Common.Section", + "elements": [ + { + "name": "api", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "subscriptions?api-version=2020-01-01" + } + }, + { + "name": "subscription", + "label": "Subscription", + "type": "Microsoft.Common.DropDown", + "defaultValue": "[first(map(steps('basics').hub.api.value, (item) => item.displayName))]", + "toolTip": "Select the subscription for your Mission Landing Zone Hub network, firewall, and remote access resources.", + "filter": true, + "constraints": { + "allowedValues": "[map(steps('basics').hub.api.value, (item) => parse(concat('{\"label\":\"', item.displayName, '\",\"value\":\"', item.id, '\",\"description\":\"', 'ID: ', item.subscriptionId, '\"}')))]", + "required": true + } + }, + { + "name": "virtualNetworksApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('basics').hub.subscription, '/providers/Microsoft.Network/virtualNetworks?api-version=2023-05-01')]" + } + }, + { + "name": "virtualNetwork", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Virtual network", + "defaultValue": "[first(map(steps('basics').hub.virtualNetworksApi.value, (item) => item.name))]", + "filter": true, + "toolTip": "Select the existing Hub virtual network.", + "constraints": { + "required": true, + "allowedValues": "[map(steps('basics').hub.virtualNetworksApi.value, (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\"}')))]" + } + }, + { + "name": "azureFirewallsApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('basics').hub.subscription, '/providers/Microsoft.Network/azureFirewalls?api-version=2023-05-01')]" + } + }, + { + "name": "azureFirewall", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Azure firewall", + "defaultValue": "[first(map(steps('basics').hub.azureFirewallsApi.value, (item) => item.name))]", + "filter": true, + "toolTip": "Select the existing Hub Azure firewall.", + "constraints": { + "required": true, + "allowedValues": "[map(steps('basics').hub.azureFirewallsApi.value, (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\"}')))]" + } + } + ] + }, + { + "name": "naming", + "type": "Microsoft.Common.Section", + "label": "Naming Components", + "elements": [ + { + "name": "description", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "The values selected below will be used as components in your naming convention to name your Azure resource groups and resources. For more information on the naming convention used in this solution, refer to the documentation.", + "link": { + "label": "https://github.com/jamasten/AzureVirtualDesktop/blob/main/docs/design/naming.md", + "uri": "https://github.com/jamasten/AzureVirtualDesktop/blob/main/docs/design/naming.md" + } + } + }, + { + "name": "identifier", + "type": "Microsoft.Common.TextBox", + "label": "Identifier", + "toolTip": "Input a 3 character identifier for the resource group and resource names created with this solution. The identifier should represent a unique value within your organization, such as a business unit or project.", + "placeholder": "Example: it1", + "constraints": { + "required": true, + "regex": "^[a-z][a-z0-9]{1,2}$", + "validationMessage": "The value must be 1 - 3 characters in length, alphanumeric, and lowercase." + } + }, + { + "name": "environment", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Environment short name", + "defaultValue": "Development (dev)", + "toolTip": "Select the target environment for the solution. The single letter environment abbreviation will be used as part of the naming convention for the resoure groups and resources.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Development (dev)", + "value": "dev" + }, + { + "label": "Production (prod)", + "value": "prod" + }, + { + "label": "Test (test)", + "value": "test" + } + ] + } + }, + { + "name": "stampIndex", + "type": "Microsoft.Common.Slider", + "label": "Stamp Index", + "defaultValue": 0, + "toolTip": "The stamp index differentiates multiple AVD stamps within the same business unit or project. For example, '0' could be used for an office workers host pool and '1' could be used for a developers host pool.", + "min": 0, + "max": 9, + "showStepMarkers": false, + "constraints": { + "required": true + }, + "visible": true + } + ] + }, + { + "name": "servicePrincipalApi", + "type": "Microsoft.Solutions.GraphApiControl", + "request": { + "method": "GET", + "path": "/v1.0/serviceprincipals?$filter=appId eq '9cdead84-a844-4324-93f2-b2e6bb768d07'" + } + } + ] + }, + { + "name": "controlPlane", + "label": "Control Plane", + "elements": [ + { + "name": "controlPlane", + "type": "Microsoft.Common.Section", + "visible": true, + "label": "Control Plane", + "elements": [ + { + "name": "locationsApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('basics').scope.subscription.id, '/locations?api-version=2022-12-01')]" + } + }, + { + "name": "providerApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('basics').scope.subscription.id, '/providers/Microsoft.DesktopVirtualization/resourceTypes?api-version=2021-04-01')]" + } + }, + { + "name": "location", + "label": "Location", + "type": "Microsoft.Common.DropDown", + "defaultValue": "[steps('basics').scope.location.displayName]", + "toolTip": "Select the location for the AVD management resources: host pool, workspace, application group, etc.", + "filter": true, + "constraints": { + "allowedValues": "[map(filter(steps('controlPlane').controlPlane.locationsApi.value, (item) => contains(first(map(filter(steps('controlPlane').controlPlane.providerApi.value, (item) => equals(item.resourceType, 'hostpools')), (item) => item.locations)), item.displayName)), (item) => parse(concat('{\"label\":\"', item.displayName, '\",\"value\":\"', item.name, '\"}')))]", + "required": true + } + } + ] + }, + { + "name": "hostPool", + "type": "Microsoft.Common.Section", + "visible": true, + "label": "Host Pool", + "elements": [ + { + "name": "validation", + "type": "Microsoft.Common.CheckBox", + "label": "Validation environment", + "toolTip": "Choose whether to deploy the host pool as a validation environment. This allows you test preview features for AVD before they are released to production.", + "constraints": { + "required": false + } + }, + { + "name": "type", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Type", + "defaultValue": "Pooled", + "multiLine": true, + "toolTip": "", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Pooled", + "value": "Pooled" + }, + { + "label": "Personal", + "value": "Personal" + } + ] + } + }, + { + "name": "loadBalancerAlgorithm", + "type": "Microsoft.Common.DropDown", + "visible": "[equals(steps('controlPlane').hostPool.type, 'Pooled')]", + "label": "Load balancing algorithm", + "defaultValue": "BreadthFirst", + "multiLine": true, + "toolTip": "Breadth-first load balancing distributes new user sessions across all available session hosts in the host pool. Depth-first load balancing distributes new user sessions to an available session host with the highest number of connections but has not reached its maximum session limit threshold.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "BreadthFirst", + "description": "Each new session is placed on the next VM. (Performance Optimized)", + "value": "BreadthFirst" + }, + { + "label": "DepthFirst", + "description": "Each new session is placed on the same VM until max sessions limit. (Cost Optimized)", + "value": "DepthFirst" + } + ] + } + }, + { + "name": "maxSessions", + "type": "Microsoft.Common.TextBox", + "visible": "[equals(steps('controlPlane').hostPool.type, 'Pooled')]", + "label": "Max session limit", + "defaultValue": "4", + "toolTip": "The maximum number of users that have concurrent sessions on a session host. When setting a host pool to have depth first load balancing or planning to use Autoscaling, you must set an appropriate max session limit according to the configuration of your deployment and capacity of your VMs.", + "constraints": { + "required": true, + "regex": "\\d+", + "validationMessage": "The value must be one or more digits." + } + }, + { + "name": "assignmentType", + "type": "Microsoft.Common.DropDown", + "visible": "[equals(steps('controlPlane').hostPool.type, 'Personal')]", + "label": "Assignment type", + "defaultValue": "Automatic (Recommended)", + "multiLine": true, + "toolTip": "Automatic assignment - The service will select an available host and assign it to an user.\nDirect assignment - Admin selects a specific host to assign to an user.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Automatic (Recommended)", + "description": "Users are assigned an available VM the first time they connect.", + "value": "Automatic" + }, + { + "label": "Direct", + "description": "An administrator assigns a VM for each individual user.", + "value": "Direct" + } + ] + } + }, + { + "name": "customRdpProperties", + "type": "Microsoft.Common.TextBox", + "visible": true, + "label": "Custom RDP properties", + "defaultValue": "audiocapturemode:i:1;camerastoredirect:s:*;use multimon:i:0;drivestoredirect:s:;", + "toolTip": "Specify the configuration for the RDP properties on the AVD host pool.", + "constraints": { + "required": true + } + }, + { + "name": "publicNetworkAccess", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Public network access", + "defaultValue": "Enabled", + "multiLine": true, + "toolTip": "Enabled: allows the host pool to be accessed from both public and private networks. Disabled: allows the host pool to only be accessed via private endpoints.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Disabled", + "value": "Disabled" + }, + { + "label": "Enabled", + "value": "Enabled" + }, + { + "label": "Enabled For Clients Only", + "value": "EnabledForClientsOnly" + }, + { + "label": "Enabled For Session Hosts Only", + "value": "EnabledForSessionHostsOnly" + } + ] + } + } + ] + }, + { + "name": "workspace", + "type": "Microsoft.Common.Section", + "visible": true, + "label": "Workspace", + "elements": [ + { + "name": "friendlyName", + "type": "Microsoft.Common.TextBox", + "label": "Friendly name (feed workspace)", + "defaultValue": "", + "placeholder": "Example: Information Technology", + "toolTip": "Input the friendly name for the AVD workspace / identifier that will be displayed in the end user's client. This value should apply to all the stamp indexes within the same identifier.", + "constraints": { + "required": false, + "regex": "^.{1,64}$", + "validationMessage": "The value must be between 1 and 64 characters in length." + }, + "visible": true + }, + { + "name": "publicNetworkAccess", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Public network access (feed workspace)", + "defaultValue": "Enabled", + "multiLine": true, + "toolTip": "Enabled: allows the host pool to be accessed from both public and private networks. Disabled: allows the host pool to only be accessed via private endpoints.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Disabled", + "value": "Disabled" + }, + { + "label": "Enabled", + "value": "Enabled" + } + ] + } + }, + { + "name": "subnetsApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('basics').hub.virtualNetwork, '/subnets?api-version=2022-05-01')]" + } + }, + { + "name": "subnet", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Hub subnet (global workspace)", + "defaultValue": "[first(map(filter(steps('controlPlane').workspace.subnetsApi.value, (item) => and(and(not(equals(item.name, 'AzureFirewallSubnet')), not(equals(item.name, 'AzureFirewallManagementSubnet'))), not(equals(item.name, 'AzureBastionSubnet')))), (item) => item.name))]", + "filter": true, + "toolTip": "Select the existing Hub subnet for the AVD Global Workspace.", + "constraints": { + "required": true, + "allowedValues": "[map(steps('controlPlane').workspace.subnetsApi.value, (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\"}')))]" + } + } + ] + }, + { + "name": "assignment", + "type": "Microsoft.Common.Section", + "label": "Assignment", + "visible": true, + "elements": [ + { + "name": "groupsApi", + "type": "Microsoft.Solutions.GraphApiControl", + "request": { + "method": "GET", + "path": "/v1.0/groups?$top=999" + } + }, + { + "name": "groupsDropDown", + "type": "Microsoft.Common.DropDown", + "label": "Groups", + "defaultValue": "", + "toolTip": "Select the desired group(s) to give access to this AVD stamp and if applicable, the FSLogix file share.", + "multiselect": true, + "filter": true, + "constraints": { + "allowedValues": "[map(steps('controlPlane').assignment.groupsApi.value, (item) => parse(concat('{\"label\":\"', item.displayName, '\",\"value\": {\"name\":\"', item.displayName, '\",\"objectId\":\"', item.id, '\"}}')))]", + "required": true + }, + "visible": "[not(empty(steps('controlPlane').assignment.groupsApi))]" + }, + { + "name": "groupsGrid", + "type": "Microsoft.Common.EditableGrid", + "ariaLabel": "Enter the security groups for access to AVD and if applicable, FSLogix. If deploying FSLogix, a storage account will be deployed for each group to support sharding.", + "label": "Security Groups", + "visible": "[empty(steps('controlPlane').assignment.groupsApi)]", + "constraints": { + "width": "Full", + "rows": { + "count": { + "min": 1, + "max": 100 + } + }, + "columns": [ + { + "id": "name", + "header": "Name", + "width": "1fr", + "element": { + "type": "Microsoft.Common.TextBox", + "placeholder": "Security Group Name", + "constraints": { + "required": true, + "validations": [] + } + } + }, + { + "id": "objectId", + "header": "Object ID", + "width": "1fr", + "element": { + "type": "Microsoft.Common.TextBox", + "placeholder": "Security Group Object ID", + "constraints": { + "required": true, + "validations": [] + } + } + } + ] + } + } + ] + }, + { + "name": "desktopFriendlyName", + "type": "Microsoft.Common.TextBox", + "label": "Desktop Friendly Name", + "defaultValue": "", + "placeholder": "Example: Help Desk", + "toolTip": "Input the friendly name for the AVD Session Desktop application that will be displayed in the end user's client.", + "constraints": { + "required": false, + "regex": "^.{1,64}$", + "validationMessage": "The value must be between 1 and 64 characters in length." + }, + "visible": true + } + ] + }, + { + "name": "hosts", + "label": "Session Hosts", + "elements": [ + { + "name": "image", + "type": "Microsoft.Common.Section", + "visible": true, + "label": "Image", + "elements": [ + { + "name": "source", + "type": "Microsoft.Common.DropDown", + "label": "Image Source", + "filter": true, + "defaultValue": "Marketplace", + "toolTip": "Select the type of image to deploy on the session hosts.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Marketplace", + "value": "marketplace" + }, + { + "label": "Compute Gallery", + "value": "gallery" + } + ] + } + }, + { + "name": "offer", + "type": "Microsoft.Common.DropDown", + "label": "Offer", + "defaultValue": "Windows 11", + "toolTip": "Select the desired marketplace image offer.", + "constraints": { + "allowedValues": [ + { + "label": "Office 365", + "value": "office-365" + }, + { + "label": "Windows 10", + "value": "Windows-10" + }, + { + "label": "Windows 11", + "value": "windows-11" + } + ], + "required": true + }, + "visible": "[equals(steps('hosts').image.source, 'marketplace')]" + }, + { + "name": "skuApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('basics').scope.subscription.id, '/providers/Microsoft.Compute/locations/', steps('basics').scope.location.name, '/publishers/MicrosoftWindowsDesktop/artifacttypes/vmimage/offers/', steps('hosts').image.offer, '/skus?api-version=2022-08-01')]" + } + }, + { + "name": "sku", + "type": "Microsoft.Common.DropDown", + "label": "SKU", + "defaultValue": "[if(equals(steps('hosts').image.offer, 'windows-11'), 'win11-23h2-avd', if(equals(steps('hosts').image.offer, 'Windows-10'),'win10-22h2-avd-g2', 'win11-23h2-avd-m365'))]", + "filter": true, + "toolTip": "Select the desired marketplace image SKU.", + "constraints": { + "allowedValues": "[map(steps('hosts').image.skuApi, (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.name, '\"}')))]", + "required": true + }, + "visible": "[equals(steps('hosts').image.source, 'marketplace')]" + }, + { + "name": "galleryImage", + "type": "Microsoft.Solutions.ResourceSelector", + "visible": "[equals(steps('hosts').image.source, 'gallery')]", + "label": "Image", + "resourceType": "Microsoft.Compute/galleries/images", + "constraints": { + "required": true + } + } + ] + }, + { + "name": "virtualMachine", + "type": "Microsoft.Common.Section", + "visible": true, + "label": "Virtual Machine", + "elements": [ + { + "name": "availability", + "type": "Microsoft.Common.DropDown", + "label": "Availability Options", + "filter": true, + "defaultValue": "Availability Zones", + "toolTip": "Select the redundancy / resiliency for the virtual machines.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Availability Sets", + "value": "AvailabilitySets" + }, + { + "label": "Availability Zones", + "value": "AvailabilityZones" + }, + { + "label": "No infrastructure redundancy required", + "value": "None" + } + ] + } + }, + { + "name": "count", + "type": "Microsoft.Common.Slider", + "label": "Count", + "defaultValue": 1, + "toolTip": "Select the number of virtual machines to deploy in your AVD host pool.", + "min": 1, + "max": 4999, + "showStepMarkers": true, + "constraints": { + "required": true + }, + "visible": true + }, + { + "name": "size", + "type": "Microsoft.Compute.SizeSelector", + "label": "Size", + "toolTip": "Select the size of the virtual machines. Multi-session hosts should have 4 - 24 vCPUs. Single session host should have 2 or more vCPUs.", + "recommendedSizes": [ + "Standard_D4ads_v5" + ], + "constraints": { + "allowedSizes": [], + "excludedSizes": [], + "numAvailabilityZonesRequired": "[if(equals(steps('hosts').virtualMachine.availability, 'AvailabilitySets'), 3, 1)]" + }, + "options": { + "hideDiskTypeFilter": false + }, + "osPlatform": "Windows", + "count": "[steps('hosts').virtualMachine.count]", + "visible": true + }, + { + "name": "diskSku", + "type": "Microsoft.Common.DropDown", + "label": "OS Disk SKU", + "filter": true, + "defaultValue": "Premium_LRS (Recommended)", + "toolTip": "Select the disk SKU for the operating system disk.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Premium_LRS (Recommended)", + "value": "Premium_LRS" + }, + { + "label": "Standard_LRS", + "value": "Standard_LRS" + }, + { + "label": "StandardSSD_LRS", + "value": "StandardSSD_LRS" + } + ] + } + } + ] + }, + { + "name": "identity", + "type": "Microsoft.Common.Section", + "label": "Identity", + "visible": true, + "elements": [ + { + "name": "solution", + "type": "Microsoft.Common.OptionsGroup", + "visible": true, + "label": "Active Directory Solution", + "defaultValue": "Active Directory Domain Services (ADDS)", + "toolTip": "Choose the Active Directory solution that already exists.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Active Directory Domain Services (ADDS)", + "value": "ActiveDirectoryDomainServices" + }, + { + "label": "Microsoft Entra ID", + "value": "MicrosoftEntraId" + }, + { + "label": "Microsoft Entra Domain Services", + "value": "MicrosoftEntraDomainServices" + } + ] + } + }, + { + "name": "intune", + "type": "Microsoft.Common.OptionsGroup", + "visible": "[equals(steps('hosts').identity.solution, 'MicrosoftEntraId')]", + "label": "Intune Enrollment", + "defaultValue": "No", + "toolTip": "If Intune is configured in your Azure AD tenant, you can choose to have the VM automatically enrolled during the deployment by selecting Yes.", + "constraints": { + "required": false, + "allowedValues": [ + { + "label": "Yes", + "value": true + }, + { + "label": "No", + "value": false + } + ] + } + }, + { + "name": "domainName", + "type": "Microsoft.Common.TextBox", + "visible": "[not(equals(steps('hosts').identity.solution, 'MicrosoftEntraId'))]", + "label": "Domain Name", + "toolTip": "Provide domain name for the selected Active Directory solution.", + "placeholder": "Example: contoso.com", + "constraints": { + "required": true + } + }, + { + "name": "ouPath", + "type": "Microsoft.Common.TextBox", + "visible": "[not(equals(steps('hosts').identity.solution, 'MicrosoftEntraId'))]", + "label": "OU Path", + "toolTip": "Input the distinguished name of the desired organization unit for the AVD session hosts.", + "placeholder": "Example: OU=pooled,OU=avd,DC=contoso,DC=com", + "constraints": { + "required": true + } + } + ] + }, + { + "name": "domainJoinCredentials", + "type": "Microsoft.Common.Section", + "label": "Domain Join Credentials", + "visible": "[not(equals(steps('hosts').identity.solution, 'MicrosoftEntraId'))]", + "elements": [ + { + "name": "userPrincipalName", + "type": "Microsoft.Common.TextBox", + "label": "User Principal Name", + "toolTip": "Enter the user principal name with domain join privileges.", + "placeholder": "Example: xadmin@contoso.com", + "constraints": { + "required": true, + "regex": "^[a-z0-9A-Z_.-]+@(?:[a-z0-9]+\\.)+[a-z]+$", + "validationMessage": "The value must be a valid user principal name." + } + }, + { + "name": "password", + "type": "Microsoft.Common.PasswordBox", + "label": { + "password": "Password" + }, + "toolTip": "Enter a password that is alphanumeric, contains at least 12 characters, 1 letter, 1 number and 1 special character.", + "constraints": { + "required": true + }, + "options": { + "hideConfirmation": true + } + } + ] + }, + { + "name": "localAdminCredentials", + "type": "Microsoft.Common.Section", + "visible": true, + "label": "Local Administrator Credential", + "elements": [ + { + "name": "username", + "type": "Microsoft.Common.TextBox", + "label": "Username", + "defaultValue": "", + "placeholder": "Example: xadmin", + "toolTip": "Input the username for the local administrator account.", + "constraints": { + "required": true, + "regex": "", + "validationMessage": "" + }, + "visible": true + }, + { + "name": "password", + "type": "Microsoft.Common.PasswordBox", + "label": { + "password": "Password", + "confirmPassword": "Confirm Password" + }, + "toolTip": "Input the password for the local administrator account.", + "constraints": { + "required": true, + "regex": "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=_!*<>()])(?=\\S+$).{12,123}$", + "validationMessage": "The value must be within 12 to 123 characters, be alphanumeric, and include 1 lower case character, 1 upper case character, 1 number, and 1 special character that is not '\\' or '-'." + }, + "options": { + "hideConfirmation": false + }, + "visible": true + } + ] + } + ] + }, + { + "name": "userProfiles", + "label": "User Profiles", + "elements": [ + { + "name": "profileSolution", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Profile Solution", + "defaultValue": "[if(equals(steps('hosts').identity.solution, 'MicrosoftEntraId'), 'Local', 'FSLogix')]", + "toolTip": "Select the user profile solution for your end users.", + "constraints": { + "required": true, + "allowedValues": "[if(equals(steps('hosts').identity.solution, 'MicrosoftEntraId'), parse('[{\"label\":\"Local\",\"value\":\"local\"}]'), parse('[{\"label\":\"FSLogix\",\"value\":\"fslogix\"},{\"label\":\"Local\",\"value\":\"local\"}]'))]" + } + }, + { + "name": "storage", + "type": "Microsoft.Common.Section", + "label": "Profile Storage", + "visible": "[equals(steps('userProfiles').profileSolution, 'fslogix')]", + "elements": [ + { + "name": "service", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Service", + "defaultValue": "Azure Files", + "toolTip": "Select the Azure storage service for the user profiles.", + "constraints": { + "required": true, + "allowedValues": "[if(equals(steps('hosts').identity.solution, 'MicrosoftEntraId'), parse('[{\"label\":\"Azure Files\",\"value\":\"AzureFiles\"}]'), parse('[{\"label\":\"Azure Files\",\"value\":\"AzureFiles\"},{\"label\":\"Azure NetApp Files\",\"value\":\"AzureNetAppFiles\"}]'))]" + } + }, + { + "name": "sku", + "type": "Microsoft.Common.DropDown", + "visible": "[equals(steps('userProfiles').profileSolution, 'fslogix')]", + "label": "SKU", + "defaultValue": "[if(equals(steps('userProfiles').profileSolution, 'local'), concat('Standard ', if(equals(steps('userProfiles').storage.service, 'AzureFiles'), '(20K IOPS)', '(100K IOPS)')), concat('Premium ', if(equals(steps('userProfiles').storage.service, 'AzureFiles'), '(100K IOPS)', '(450K IOPS)')))]", + "toolTip": "Select the storage SKU for the SMB file shares to support the use of FSLogix.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "[concat('Premium ', if(equals(steps('userProfiles').storage.service, 'AzureFiles'), '(100K IOPS)', '(450K IOPS)'))]", + "value": "Premium" + }, + { + "label": "[concat('Standard ', if(equals(steps('userProfiles').storage.service, 'AzureFiles'), '(20K IOPS)', '(320K IOPS)'))]", + "value": "Standard" + } + ] + } + }, + { + "name": "fileShareSize", + "type": "Microsoft.Common.Slider", + "label": "File share size (GB)", + "defaultValue": 100, + "toolTip": "Input the quota size for the SMB file share to support the use of FSLogix.", + "min": 100, + "max": 100000, + "showStepMarkers": false, + "constraints": { + "required": true + }, + "visible": "[equals(steps('userProfiles').profileSolution, 'fslogix')]" + }, + { + "name": "fslogixContainerType", + "type": "Microsoft.Common.DropDown", + "visible": "[equals(steps('userProfiles').profileSolution, 'fslogix')]", + "label": "FSLogix solution", + "defaultValue": "Profile Container", + "toolTip": "Select the solution for the FSLogix profiles.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Cloud Cache, Profile Container", + "value": "CloudCacheProfileContainer" + }, + { + "label": "Cloud Cache, Profile & Office Container", + "value": "CloudCacheProfileOfficeContainer" + }, + { + "label": "Profile Container", + "value": "ProfileContainer" + }, + { + "label": "Profile & Office Container", + "value": "ProfileOfficeContainer" + } + ] + } + } + ] + } + ] + }, + { + "name": "networking", + "label": "Networking", + "elements": [ + { + "name": "controlPlane", + "label": "[if(equals(steps('basics').scope.location.name, steps('controlPlane').controlPlane.location), 'Control Plane & Session Hosts', 'Control Plane')]", + "type": "Microsoft.Common.Section", + "visible": true, + "elements": [ + { + "name": "virtualNetworkAddressCidrRange", + "label": "Virtual network CIDR range", + "type": "Microsoft.Common.TextBox", + "defaultValue": "10.0.121.0/24", + "toolTip": "Specify an address CIDR range within the range [10,26].", + "constraints": { + "required": true, + "validations": [ + { + "regex": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\/(1[0-9]|2[0-6]))$", + "message": "Invalid CIDR range. The address prefix must be in the range [10,26]." + } + ] + } + }, + { + "name": "subnetAddressCidrRangeWorkload", + "label": "Subnet CIDR range (Workload)", + "type": "Microsoft.Common.TextBox", + "defaultValue": "[concat('10.0.121.0/2', if(and(equals(steps('basics').scope.location.name, steps('controlPlane').controlPlane.location), equals(steps('userProfiles').storage.service, 'AzureNetAppFiles')), '5', '4'))]", + "toolTip": "Specify a CIDR range for the workload subnet within the AVD spoke virtual network range [24].", + "constraints": { + "required": true, + "validations": [ + { + "regex": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\/(2[4-8]))$", + "message": "Invalid CIDR range. The address prefix must be in the range [24,28]." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), 8), equals(last(take(split(first(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), '.'), 1)), last(take(split(first(split(steps('networking').controlPlane.subnetAddressCidrRangeWorkload, '/')), '.'), 1))), true)]", + "message": "CIDR range not within virtual network CIDR range (first octet)." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), 16), equals(last(take(split(first(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), '.'), 2)), last(take(split(first(split(steps('networking').controlPlane.subnetAddressCidrRangeWorkload, '/')), '.'), 2))), true)]", + "message": "CIDR range not within virtual network CIDR range (second octet)." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), 24), equals(last(take(split(first(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), '.'), 3)), last(take(split(first(split(steps('networking').controlPlane.subnetAddressCidrRangeWorkload, '/')), '.'), 3))), true)]", + "message": "CIDR range not within virtual network CIDR range (third octet)." + }, + { + "isValid": "[lessOrEquals(last(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), last(split(steps('networking').controlPlane.subnetAddressCidrRangeWorkload, '/')))]", + "message": "CIDR range not within virtual network CIDR range (subnet mask)." + } + ] + } + }, + { + "name": "subnetAddressCidrRangeAnf", + "label": "Subnet CIDR range (Azure NetApp Files)", + "type": "Microsoft.Common.TextBox", + "visible": "[and(equals(steps('basics').scope.location.name, steps('controlPlane').controlPlane.location), equals(steps('userProfiles').storage.service, 'AzureNetAppFiles'))]", + "defaultValue": "10.0.121.128/25", + "toolTip": "Specify a CIDR range for the Azure NetApp Files subnet within the AVD spoke virtual network range [24].", + "constraints": { + "required": true, + "validations": [ + { + "regex": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\/(2[4-8]))$", + "message": "Invalid CIDR range. The address prefix must be in the range [24,28]." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), 8), equals(last(take(split(first(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), '.'), 1)), last(take(split(first(split(steps('networking').controlPlane.subnetAddressCidrRangeAnf, '/')), '.'), 1))), true)]", + "message": "CIDR range not within virtual network CIDR range (first octet)." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), 16), equals(last(take(split(first(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), '.'), 2)), last(take(split(first(split(steps('networking').controlPlane.subnetAddressCidrRangeAnf, '/')), '.'), 2))), true)]", + "message": "CIDR range not within virtual network CIDR range (second octet)." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), 24), equals(last(take(split(first(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), '.'), 3)), last(take(split(first(split(steps('networking').controlPlane.subnetAddressCidrRangeAnf, '/')), '.'), 3))), true)]", + "message": "CIDR range not within virtual network CIDR range (third octet)." + }, + { + "isValid": "[lessOrEquals(last(split(steps('networking').controlPlane.virtualNetworkAddressCidrRange, '/')), last(split(steps('networking').controlPlane.subnetAddressCidrRangeAnf, '/')))]", + "message": "CIDR range not within virtual network CIDR range (subnet mask)." + } + ] + } + }, + { + "name": "disableBgpRoutePropagation", + "type": "Microsoft.Common.CheckBox", + "visible": true, + "label": "Disable BGP route propagation", + "defaultValue": true, + "toolTip": "Choose whether to disable BGP route propagation on the route table." + } + ] + }, + { + "name": "hosts", + "label": "Session Hosts", + "type": "Microsoft.Common.Section", + "visible": "[not(equals(steps('basics').scope.location.name, steps('controlPlane').controlPlane.location))]", + "elements": [ + { + "name": "virtualNetworkAddressCidrRange", + "label": "Virtual network CIDR range", + "type": "Microsoft.Common.TextBox", + "defaultValue": "10.0.122.0/24", + "toolTip": "Specify an address CIDR range within the range [10,24].", + "constraints": { + "required": true, + "validations": [ + { + "regex": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\/(1[0-9]|2[0-6]))$", + "message": "Invalid CIDR range. The address prefix must be in the range [10,26]." + } + ] + } + }, + { + "name": "subnetAddressCidrRangeWorkload", + "label": "Subnet CIDR range (Workload)", + "type": "Microsoft.Common.TextBox", + "defaultValue": "[concat('10.0.122.0/2', if(and(not(equals(steps('basics').scope.location.name, steps('controlPlane').controlPlane.location)), equals(steps('userProfiles').storage.service, 'AzureNetAppFiles')), '5', '4'))]", + "toolTip": "Specify a CIDR range for the default subnet within the AVD Hosts spoke virtual network range [24].", + "constraints": { + "required": true, + "validations": [ + { + "regex": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\/(2[4-8]))$", + "message": "Invalid CIDR range. The address prefix must be in the range [26,28]." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), 8), equals(last(take(split(first(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), '.'), 1)), last(take(split(first(split(steps('networking').hosts.subnetAddressCidrRangeWorkload, '/')), '.'), 1))), true)]", + "message": "CIDR range not within virtual network CIDR range (first octet)." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), 16), equals(last(take(split(first(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), '.'), 2)), last(take(split(first(split(steps('networking').hosts.subnetAddressCidrRangeWorkload, '/')), '.'), 2))), true)]", + "message": "CIDR range not within virtual network CIDR range (second octet)." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), 24), equals(last(take(split(first(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), '.'), 3)), last(take(split(first(split(steps('networking').hosts.subnetAddressCidrRangeWorkload, '/')), '.'), 3))), true)]", + "message": "CIDR range not within virtual network CIDR range (third octet)." + }, + { + "isValid": "[lessOrEquals(last(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), last(split(steps('networking').hosts.subnetAddressCidrRangeWorkload, '/')))]", + "message": "CIDR range not within virtual network CIDR range (subnet mask)." + } + ] + } + }, + { + "name": "subnetAddressCidrRangeAnf", + "label": "Subnet CIDR range (Azure NetApp Files)", + "type": "Microsoft.Common.TextBox", + "visible": "[and(not(equals(steps('basics').scope.location.name, steps('controlPlane').controlPlane.location)), equals(steps('userProfiles').storage.service, 'AzureNetAppFiles'))]", + "defaultValue": "10.0.122.128/25", + "toolTip": "Specify a CIDR range for the Azure NetApp Files subnet within the AVD spoke virtual network range [26].", + "constraints": { + "required": true, + "validations": [ + { + "regex": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\/(2[4-8]))$", + "message": "Invalid CIDR range. The address prefix must be in the range [24,28]." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), 8), equals(last(take(split(first(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), '.'), 1)), last(take(split(first(split(steps('networking').hosts.subnetAddressCidrRangeAnf, '/')), '.'), 1))), true)]", + "message": "CIDR range not within virtual network CIDR range (first octet)." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), 16), equals(last(take(split(first(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), '.'), 2)), last(take(split(first(split(steps('networking').hosts.subnetAddressCidrRangeAnf, '/')), '.'), 2))), true)]", + "message": "CIDR range not within virtual network CIDR range (second octet)." + }, + { + "isValid": "[if(greaterOrEquals(last(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), 24), equals(last(take(split(first(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), '.'), 3)), last(take(split(first(split(steps('networking').hosts.subnetAddressCidrRangeAnf, '/')), '.'), 3))), true)]", + "message": "CIDR range not within virtual network CIDR range (third octet)." + }, + { + "isValid": "[lessOrEquals(last(split(steps('networking').hosts.virtualNetworkAddressCidrRange, '/')), last(split(steps('networking').hosts.subnetAddressCidrRangeAnf, '/')))]", + "message": "CIDR range not within virtual network CIDR range (subnet mask)." + } + ] + } + } + ] + } + ] + }, + { + "name": "management", + "label": "Management", + "elements": [ + { + "name": "startVmOnConnect", + "type": "Microsoft.Common.Section", + "label": "Start VM On Connect", + "visible": "[empty(steps('basics').servicePrincipalApi)]", + "elements": [ + { + "name": "objectId", + "type": "Microsoft.Common.TextBox", + "label": "AVD Object ID", + "visible": true, + "defaultValue": "", + "placeholder": "", + "toolTip": "Input the object ID for the Azure Virtual Desktop enterprise application in Entra ID. The application ID for the principal is 9cdead84-a844-4324-93f2-b2e6bb768d07.", + "constraints": { + "required": true, + "validations": [ + { + "regex": "^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$", + "message": "The value must be a globally unique ID." + } + ] + } + } + ] + }, + { + "name": "drainMode", + "type": "Microsoft.Common.Section", + "label": "Drain Mode", + "visible": true, + "elements": [ + { + "name": "enable", + "type": "Microsoft.Common.CheckBox", + "label": "Enable drain mode?", + "defaultValue": false, + "toolTip": "Enables drain mode on the AVD session hosts so the virtual machines cannot be accessed until they have been validated." + } + ] + }, + { + "name": "scaling", + "type": "Microsoft.Common.Section", + "label": "Scaling", + "visible": "[equals(steps('controlPlane').hostPool.type, 'Pooled')]", + "elements": [ + { + "name": "enable", + "type": "Microsoft.Common.CheckBox", + "visible": true, + "label": "Enable scaling tool?", + "defaultValue": false, + "toolTip": "Choose whether to deploy the required resources to enable the Scaling Tool." + }, + { + "name": "beginPeakTime", + "type": "Microsoft.Common.TextBox", + "label": "Begin Peak Time", + "visible": "[steps('management').scaling.enable]", + "defaultValue": "8:00", + "placeholder": "", + "toolTip": "Input the time when peak hours begin for your end users.", + "constraints": { + "required": true, + "validations": [ + { + "regex": "^[012]?[0-9]:[0-5][0-9]$", + "message": "The value must be in a proper time format with a two digit hour and two digit minutes, e.g. 12am should be input as 00:00." + } + ] + } + }, + { + "name": "endPeakTime", + "type": "Microsoft.Common.TextBox", + "label": "End Peak Time", + "visible": "[steps('management').scaling.enable]", + "defaultValue": "17:00", + "placeholder": "", + "toolTip": "Input the time when peak hours end for your end users.", + "constraints": { + "required": true, + "validations": [ + { + "regex": "^[012]?[0-9]:[0-5][0-9]$", + "message": "The value must be in a proper time format with a two digit hour and two digit minutes, e.g. 8pm should be input as 20:00." + } + ] + } + }, + { + "name": "forceLogOff", + "type": "Microsoft.Common.TextBox", + "label": "Force Log Off (Seconds)", + "visible": "[steps('management').scaling.enable]", + "defaultValue": "0", + "toolTip": "Use this setting to force logoff users if session time limit settings cannot be used.", + "placeholder": "", + "multiLine": false, + "constraints": { + "required": true, + "validations": [ + { + "regex": "^[0-9]{1,3}$", + "message": "The value must be between 1 and 3 digits." + } + ] + } + }, + { + "name": "minimumHosts", + "type": "Microsoft.Common.TextBox", + "label": "Minimum Number of Session Hosts", + "visible": "[steps('management').scaling.enable]", + "defaultValue": "0", + "toolTip": "Use this setting to determine the minimum number of sessions hosts to keep online.", + "placeholder": "", + "multiLine": false, + "constraints": { + "required": true, + "validations": [ + { + "regex": "^[0-9]{1,4}$", + "message": "The value must be between 1 and 4 digits." + } + ] + } + }, + { + "name": "cpuThreshold", + "type": "Microsoft.Common.TextBox", + "label": "Session Threshold Per CPU", + "visible": "[steps('management').scaling.enable]", + "defaultValue": "1", + "toolTip": "Use this setting to determine the number of sessions per CPU before turning on another host.", + "placeholder": "", + "multiLine": false, + "constraints": { + "required": true, + "validations": [ + { + "regex": "^[0-9]{0,1}(?:\\.[0-9]{0,2})?$", + "message": "The value must be a number with 1 whole number and, if desired, a single or two digit decimal number." + } + ] + } + } + ] + }, + { + "name": "backup", + "type": "Microsoft.Common.Section", + "label": "Backup", + "visible": "[or(equals(steps('userProfiles').profileSolution, 'local'), and(equals(steps('userProfiles').profileSolution, 'fslogix'), equals(steps('userProfiles').storage.service, 'AzureFiles')))]", + "elements": [ + { + "name": "recoveryServices", + "type": "Microsoft.Common.CheckBox", + "visible": true, + "label": "Enable recovery services?", + "defaultValue": false, + "toolTip": "Choose to deploy backups for your solution. For a personal host pool, this will enable backups on the virtual machines. For a pooled host pool, this will enable backups on the FSLogix file share when using Azure Files." + } + ] + }, + { + "name": "monitoring", + "type": "Microsoft.Common.Section", + "label": "Monitoring", + "visible": true, + "elements": [ + { + "name": "enable", + "type": "Microsoft.Common.CheckBox", + "visible": true, + "label": "Enable monitoring for AVD Insights?", + "defaultValue": false, + "toolTip": "Deploy the required resources to enable AVD Insights." + }, + { + "name": "agent", + "type": "Microsoft.Common.DropDown", + "visible": "[steps('management').monitoring.enable]", + "label": "Monitoring agent", + "defaultValue": "Log Analytics Agent", + "toolTip": "Select the solution for the FSLogix profiles.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Azure Monitor Agent", + "value": "AzureMonitorAgent" + }, + { + "label": "Log Analytics Agent", + "value": "LogAnalyticsAgent" + } + ] + } + }, + { + "name": "enableSecurity", + "type": "Microsoft.Common.CheckBox", + "visible": "[and(steps('management').monitoring.enable, equals(steps('management').monitoring.agent, 'LogAnalyticsAgent'))]", + "label": "Multi-home agent for security monitoring?", + "defaultValue": false, + "toolTip": "Deploy the required configuration to multi-home the Microsoft Monitoring Agent for security monitoring." + }, + { + "name": "logAnalyticsWorkspace", + "type": "Microsoft.Solutions.ResourceSelector", + "label": "Existing Log Analytics Workspace for Security", + "visible": "[and(and(steps('management').monitoring.enable, equals(steps('management').monitoring.agent, 'LogAnalyticsAgent')),steps('management').monitoring.enableSecurity)]", + "resourceType": "Microsoft.OperationalInsights/workspaces", + "toolTip": "Select the log analytics workspace used for collecting security data for Sentinel or Defender for Cloud. This is required to multihome the Microsoft Monitoring Agent.", + "options": {} + } + ] + } + ] + }, + { + "name": "artifacts", + "label": "Artifacts", + "elements": [ + { + "name": "storage", + "type": "Microsoft.Common.Section", + "label": "Artifacts storage", + "elements": [ + { + "name": "description", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Select the storage account and container that hosts the artifacts required to deploy this solution." + } + }, + { + "name": "storageAccount", + "type": "Microsoft.Solutions.ResourceSelector", + "label": "Existing storage account", + "resourceType": "Microsoft.Storage/storageAccounts", + "options": { + "filter": { + "subscription": "all", + "location": "all" + } + } + }, + { + "name": "containerApi", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('artifacts').storage.storageAccount.id, '/blobServices/default/containers?api-version=2023-01-01')]" + } + }, + { + "name": "container", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Existing container", + "defaultValue": "", + "filter": true, + "toolTip": "Select the existing container containing the required artifacts.", + "constraints": { + "required": true, + "allowedValues": "[map(steps('artifacts').storage.containerApi.value, (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.name, '\"}')))]" + } + } + ], + "visible": true + }, + { + "name": "files", + "type": "Microsoft.Common.Section", + "label": "Artifacts file names", + "elements": [ + { + "name": "description", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Input the file names for the required artifacts from the specified Azure Blobs container above." + } + }, + { + "name": "avdAgentMsiName", + "type": "Microsoft.Common.TextBox", + "label": "Azure Virtual Desktop Agent (.msi)", + "defaultValue": "Microsoft.RDInfra.RDAgent.Installer-x64-1.0.7909.2600.msi", + "toolTip": "Input the file / blob name for the AVD Agent installer.", + "placeholder": "", + "multiLine": false, + "constraints": { + "required": true, + "validations": [ + { + "isValid": "[endsWith(steps('artifacts').files.avdAgentMsiName, '.msi')]", + "message": "The file name must end with '.msi'." + } + ] + }, + "visible": true + }, + { + "name": "avdAgentBootLoaderMsiName", + "type": "Microsoft.Common.TextBox", + "label": "Azure Virtual Desktop Boot Loader (.msi)", + "defaultValue": "Microsoft.RDInfra.RDAgentBootLoader.Installer-x64 (5).msi", + "toolTip": "Input the file / blob name for the AVD Boot Loader installer.", + "placeholder": "", + "multiLine": false, + "constraints": { + "required": true, + "validations": [ + { + "isValid": "[endsWith(steps('artifacts').files.avdAgentBootLoaderMsiName, '.msi')]", + "message": "The file name must end with '.msi'." + } + ] + }, + "visible": true + }, + { + "name": "azurePowerShellModuleMsiName", + "type": "Microsoft.Common.TextBox", + "label": "Azure PowerShell Module Installer (.msi)", + "defaultValue": "Az-Cmdlets-10.2.0.37547-x64.msi", + "toolTip": "Input the file / blob name for the Azure PowerShell Module installer.", + "placeholder": "", + "multiLine": false, + "constraints": { + "required": true, + "validations": [ + { + "isValid": "[endsWith(steps('artifacts').files.azurePowerShellModuleMsiName, '.msi')]", + "message": "The file name must end with '.msi'." + } + ] + }, + "visible": true + }, + { + "name": "filesWarning", + "type": "Microsoft.Common.InfoBox", + "visible": true, + "options": { + "style": "Warning", + "text": "The files listed above are prerequisites for this solution. They must be downloaded and staged in Azure Blob storage. Once staged, ensure the file names listed above match the file names in Azure Blob storage since the names can change over time. Refer to the following link to download the files:", + "uri": { + "text": "https://github.com/jamasten/AzureVirtualDesktop/blob/main/docs/prerequisites.md#required", + "value": "https://github.com/jamasten/AzureVirtualDesktop/blob/main/docs/prerequisites.md#required" + } + } + } + ] + } + ] + }, + { + "name": "tags", + "label": "Tags", + "elements": [ + { + "name": "tags", + "type": "Microsoft.Common.TagsByResource", + "resources": [ + "Microsoft.Automation/automationAccounts", + "Microsoft.Compute/availabilitySets", + "Microsoft.Compute/diskAccesses", + "Microsoft.Compute/diskEncryptionSets", + "Microsoft.Compute/virtualMachines", + "Microsoft.DesktopVirtualization/applicationGroups", + "Microsoft.DesktopVirtualization/hostPools", + "Microsoft.Insights/dataCollectionRules", + "Microsoft.KeyVault/vaults", + "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.NetApp/netAppAccounts", + "Microsoft.Network/networkInterfaces", + "Microsoft.Network/networkSecurityGroups", + "Microsoft.Network/privateEndpoints", + "Microsoft.Network/routeTables", + "Microsoft.Network/virtualNetworks", + "Microsoft.OperationalInsights/workspaces", + "Microsoft.RecoveryServices/vaults", + "Microsoft.Resources/resourceGroups", + "Microsoft.Storage/storageAccounts" + ] + } + ] + } + ] + }, + "outputs": { + "parameters": { + "activeDirectorySolution": "[if(and(equals(steps('hosts').identity.solution, 'MicrosoftEntraId'), steps('hosts').identity.intune), 'MicrosoftEntraIdIntuneEnrollment', steps('hosts').identity.solution)]", + "artifactsContainerName": "[steps('artifacts').storage.container]", + "artifactsStorageAccountResourceId": "[steps('artifacts').storage.storageAccount.id]", + "availability": "[steps('hosts').virtualMachine.availability]", + "avdAgentMsiName": "[steps('artifacts').files.avdAgentMsiName.blobName]", + "avdAgentBootLoaderMsiName": "[steps('artifacts').files.avdAgentBootLoaderMsiName.blobName]", + "avdObjectId": "[if(empty(steps('basics').servicePrincipalApi), steps('management').startVmOnConnect.objectId, first(map(steps('basics').servicePrincipalApi.value, (item) => item.id)))]", + "azurePowerShellModuleMsiName": "[steps('artifacts').files.azurePowerShellModuleMsiName.blobName]", + "azureNetAppFilesSubnetAddressPrefix": "[if(and(equals(steps('basics').scope.location.name, steps('controlPlane').controlPlane.location), equals(steps('userProfiles').storage.service, 'AzureNetAppFiles')), steps('networking').controlPlane.subnetAddressCidrRangeAnf, steps('networking').hosts.subnetAddressCidrRangeAnf)]", + "customRdpProperty": "[steps('controlPlane').hostPool.customRdpProperties]", + "desktopFriendlyName": "[steps('controlPlane').desktopFriendlyName]", + "disableBgpRoutePropagation": "[steps('networking').controlPlane.disableBgpRoutePropagation]", + "diskSku": "[steps('hosts').virtualMachine.diskSku]", + "domainJoinPassword": "[steps('hosts').domainJoinCredentials.password]", + "domainJoinUserPrincipalName": "[steps('hosts').domainJoinCredentials.userPrincipalName]", + "domainName": "[steps('hosts').identity.domainName]", + "drainMode": "[steps('management').drainMode.enable]", + "environmentShortName": "[steps('basics').naming.environment]", + "fslogixShareSizeInGB": "[if(equals(steps('userProfiles').profileSolution, 'local'), 100, steps('userProfiles').storage.fileShareSize)]", + "fslogixContainerType": "[steps('userProfiles').storage.fslogixContainerType]", + "fslogixStorageService": "[if(equals(steps('userProfiles').profileSolution, 'local'), 'None', concat( steps('userProfiles').storage.service, ' ', steps('userProfiles').storage.sku))]", + "hostPoolPublicNetworkAccess": "[steps('controlPlane').hostPool.publicNetworkAccess]", + "hostPoolType": "[if(equals(steps('controlPlane').hostPool.type, 'Pooled'), concat(steps('controlPlane').hostPool.type, ' ', steps('controlPlane').hostPool.loadBalancerAlgorithm), concat(steps('controlPlane').hostPool.type, ' ', steps('controlPlane').hostPool.assignmentType))]", + "hubAzureFirewallResourceId": "[steps('basics').hub.azureFirewall]", + "hubSubnetResourceId": "[steps('controlPlane').workspace.subnet]", + "hubVirtualNetworkResourceId": "[steps('basics').hub.virtualNetwork]", + "identifier": "[steps('basics').naming.identifier]", + "imageDefinitionResourceId": "[if(equals(steps('hosts').image.source, 'gallery'), steps('hosts').image.galleryImage.id, '')]", + "imageOffer": "[steps('hosts').image.offer]", + "imagePublisher": "MicrosoftWindowsDesktop", + "imageSku": "[steps('hosts').image.sku]", + "locationControlPlane": "[steps('controlPlane').controlPlane.location]", + "locationVirtualMachines": "[steps('basics').scope.location.name]", + "logAnalyticsWorkspaceRetention": 30, + "logAnalyticsWorkspaceSku": "PerGB2018", + "maxSessionLimit": "[if(equals(steps('controlPlane').hostPool.type, 'Pooled'), steps('controlPlane').hostPool.maxSessions, 1)]", + "monitoring": "[steps('management').monitoring.enable]", + "organizationalUnitPath": "[if(equals(steps('hosts').identity.solution, 'MicrosoftEntraId'), '', steps('hosts').identity.ouPath)]", + "recoveryServices": "[steps('management').backup.recoveryServices]", + "scalingBeginPeakTime": "[if(steps('management').scaling.enable, steps('management').scaling.beginPeakTime, '9:00')]", + "scalingEndPeakTime": "[if(steps('management').scaling.enable, steps('management').scaling.endPeakTime, '17:00')]", + "scalingLimitSecondsToForceLogOffUser": "[if(steps('management').scaling.enable, steps('management').scaling.forceLogOff, '0')]", + "scalingMinimumNumberOfRdsh": "[if(steps('management').scaling.enable, steps('management').scaling.minimumHosts, '0')]", + "scalingSessionThresholdPerCPU": "[if(steps('management').scaling.enable, steps('management').scaling.cpuThreshold, '1')]", + "scalingTool": "[steps('management').scaling.enable]", + "securityLogAnalyticsWorkspaceResourceId": "[if(steps('management').monitoring.enableSecurity, steps('management').monitoring.logAnalyticsWorkspace.id, '')]", + "securityPrincipals": "[if(empty(steps('controlPlane').assignment.groupsApi), steps('controlPlane').assignment.groupsGrid, steps('controlPlane').assignment.groupsDropDown)]", + "sessionHostCount": "[steps('hosts').virtualMachine.count]", + "sessionHostIndex": 0, + "stampIndex": "[steps('basics').naming.stampIndex]", + "storageCount": "[if(equals(steps('userProfiles').profileSolution, 'local'), 0, if(empty(steps('controlPlane').assignment.groupsApi), length(steps('controlPlane').assignment.groupsGrid), length(steps('controlPlane').assignment.groupsDropDown)))]", + "storageIndex": 0, + "subnetAddressPrefixes": "[if(equals(steps('basics').scope.location.name, steps('controlPlane').controlPlane.location), parse(concat('[\"', steps('networking').controlPlane.subnetAddressCidrRangeWorkload, '\"]')), parse(concat('[\"', steps('networking').controlPlane.subnetAddressCidrRangeWorkload, '\", \"', steps('networking').hosts.subnetAddressCidrRangeWorkload, '\"]')))]", + "tags": "[steps('tags').tags]", + "validationEnvironment": "[steps('controlPlane').hostPool.validation]", + "virtualMachineMonitoringAgent": "[steps('management').monitoring.agent]", + "virtualMachinePassword": "[steps('hosts').localAdminCredentials.password]", + "virtualMachineSize": "[steps('hosts').virtualMachine.size]", + "virtualMachineUsername": "[steps('hosts').localAdminCredentials.username]", + "virtualNetworkAddressPrefixes": "[if(equals(steps('basics').scope.location.name, steps('controlPlane').controlPlane.location), parse(concat('[\"', steps('networking').controlPlane.virtualNetworkAddressCidrRange, '\"]')), parse(concat('[\"', steps('networking').controlPlane.virtualNetworkAddressCidrRange, '\", \"', steps('networking').hosts.virtualNetworkAddressCidrRange, '\"]')))]", + "workspaceFriendlyName": "[steps('controlPlane').workspace.friendlyName]", + "workspacePublicNetworkAccess": "[steps('controlPlane').workspace.publicNetworkAccess]" + }, + "kind": "Subscription", + "location": "[steps('basics').scope.location.name]", + "subscriptionId": "[steps('basics').scope.subscription.id]" + } + } +} \ No newline at end of file