diff --git a/templates/workspace_services/avd-aad/.dockerignore b/templates/workspace_services/avd-aad/.dockerignore new file mode 100644 index 0000000000..cb804790e6 --- /dev/null +++ b/templates/workspace_services/avd-aad/.dockerignore @@ -0,0 +1 @@ +Dockerfile.tmpl \ No newline at end of file diff --git a/templates/workspace_services/avd-aad/.env.sample b/templates/workspace_services/avd-aad/.env.sample new file mode 100644 index 0000000000..c10799a6d1 --- /dev/null +++ b/templates/workspace_services/avd-aad/.env.sample @@ -0,0 +1,11 @@ +WORKSPACE_ID=__CHANGE_ME__ +ID=__CHANGE_ME__ +AZURE_LOCATION=__CHANGE_ME__ +LOCAL_ADMIN_NAME="adminuser" +VM_SIZE="Standard_D2s_v3" +VM_COUNT="1" +VM_LICENSE_TYPE="Windows_Client" + +ARM_CLIENT_ID=__CHANGE_ME__ +ARM_CLIENT_SECRET=__CHANGE_ME__ +ARM_TENANT_ID=__CHANGE_ME__ diff --git a/templates/workspace_services/avd-aad/Dockerfile.tmpl b/templates/workspace_services/avd-aad/Dockerfile.tmpl new file mode 100644 index 0000000000..c1658e5170 --- /dev/null +++ b/templates/workspace_services/avd-aad/Dockerfile.tmpl @@ -0,0 +1,13 @@ +FROM debian:buster + +# PORTER_MIXINS + +ARG BUNDLE_DIR + +COPY . $BUNDLE_DIR + +WORKDIR $BUNDLE_DIR + +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 + +RUN az bicep build --file ./bicep/main.bicep --outfile main.json diff --git a/templates/workspace_services/avd-aad/README.md b/templates/workspace_services/avd-aad/README.md new file mode 100644 index 0000000000..1d51c43bdb --- /dev/null +++ b/templates/workspace_services/avd-aad/README.md @@ -0,0 +1,27 @@ +# azure-virtual-desktop-bicep + +AzureAD-Joined session hosts requires the following: +- Non-overlapping private IP space +- Permissions to grant users the "Virtual Machine User Login" role at the resource group level for to login + +## Resources + +### Learning Bicep + +- [Define resources with Bicep and ARM templates](https://docs.microsoft.com/azure/templates) +- [Bicep modules](https://docs.microsoft.com/azure/azure-resource-manager/templates/bicep-modules) +- [Iterative loops in Bicep](https://docs.microsoft.com/azure/azure-resource-manager/bicep/loop-resources#resource-iteration-with-condition) + +### Specific Azure Resource Definitions in Bicep + +- [Bicep reference: Microsoft.DesktopVirtualization/hostPools](https://docs.microsoft.com/azure/templates/microsoft.desktopvirtualization/hostpools?tabs=bicep) +- [Bicep reference: Microsoft.DesktopVirtualization/applicationGroups](https://docs.microsoft.com/azure/templates/microsoft.desktopvirtualization/applicationgroups?tabs=bicep) +- [Bicep reference: Microsoft.DesktopVirtualization/workspaces](https://docs.microsoft.com/azure/templates/microsoft.desktopvirtualization/workspaces?tabs=bicep) +- [ARM reference: Microsoft.DesktopVirtualization@2021-07-12](https://github.com/Azure/bicep-types-az/blob/main/generated/desktopvirtualization/microsoft.desktopvirtualization/2021-07-12/types.md) +- [ARM template for VM creation](https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/armtemplates/Hostpool_12-9-2021/nestedTemplates/managedDisks-galleryvm.json) + +## Some notes + +The `Microsoft.DesktopVirtualization` namespace isn't well documented yet, so I recommend you reference the [REST API docs](https://docs.microsoft.com/rest/api/desktopvirtualization/) to determine which API versions you should be using. + +To research common VM extension error messages, see [Understand common error messages when you manage virtual machines in Azure](https://docs.microsoft.com/troubleshoot/azure/virtual-machines/error-messages). diff --git a/templates/workspace_services/avd-aad/bicep/bicepconfig.json b/templates/workspace_services/avd-aad/bicep/bicepconfig.json new file mode 100644 index 0000000000..93bb8bb405 --- /dev/null +++ b/templates/workspace_services/avd-aad/bicep/bicepconfig.json @@ -0,0 +1,17 @@ +{ + "analyzers": { + "core": { + "verbose": false, + "enabled": true, + "rules": { + "no-hardcoded-env-urls": { + "level": "warning", + "excludedhosts": [ + "schema.management.azure.com", + "wvdportalstorageblob.blob.core.windows.net" + ] + } + } + } + } +} diff --git a/templates/workspace_services/avd-aad/bicep/main.bicep b/templates/workspace_services/avd-aad/bicep/main.bicep new file mode 100644 index 0000000000..361dcd966f --- /dev/null +++ b/templates/workspace_services/avd-aad/bicep/main.bicep @@ -0,0 +1,117 @@ +targetScope = 'subscription' + +param id string +param workspaceId string +param treId string +param tags object = {} +param localAdminName string = 'adminuser' +param vmSize string = 'Standard_D2as_v4' + +param vmCount int = 1 +param deploymentTime string = utcNow() +@secure() +param passwordSeed string = newGuid() + +var shortWorkspaceId = substring(workspaceId, length(workspaceId) - 4, 4) +var shortServiceId = substring(id, length(id) - 4, 4) +var workspaceResourceNameSuffix = '${treId}-ws-${shortWorkspaceId}' +var serviceResourceNameSuffix = '${workspaceResourceNameSuffix}-svc-${shortServiceId}' + +var deploymentNamePrefix = '${serviceResourceNameSuffix}-{rtype}-${deploymentTime}' + +resource workspaceResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' existing = { + name: 'rg-${workspaceResourceNameSuffix}' +} + +resource workspaceKeyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = { + name: 'kv-${workspaceResourceNameSuffix}' + scope: workspaceResourceGroup +} + +// Doubling up the unique string with the same seed does not increase password entropy, +// but it guarantees that there will be at least three character classes present in the password +// to meet operating system password complexity requirements +// This could be enhanced by specifying a second, different seed GUID +var localAdminPasswordGenerated = '${uniqueString(passwordSeed)}_${toUpper(uniqueString(passwordSeed))}' + +var secrets = [ + { + secretValue: passwordSeed + secretName: '${shortServiceId}-${deploymentTime}-localadminpwdseed' + } + { + // Generate a new password for the required local VM admin + secretValue: localAdminPasswordGenerated + secretName: '${shortServiceId}-${deploymentTime}-localadminpwd' + } +] + +// Persist the new password in the workspace's Key Vault +module keyVaultSecrets 'modules/keyVaultSecret.bicep' = [for (secret, i) in secrets: { + scope: workspaceResourceGroup + name: '${replace(deploymentNamePrefix, '{rtype}', 'Secret')}-${i}' + params: { + workspaceKeyVaultName: workspaceKeyVault.name + secretValue: secrets[i].secretValue + secretName: secrets[i].secretName + } +}] + +resource workspaceVirtualNetwork 'Microsoft.Network/virtualNetworks@2019-11-01' existing = { + scope: workspaceResourceGroup + name: 'vnet-${workspaceResourceNameSuffix}' +} + +module hostPool 'modules/hostPools.bicep' = { + scope: workspaceResourceGroup + name: replace(deploymentNamePrefix, '{rtype}', 'AVD-HostPool') + params: { + name: serviceResourceNameSuffix + tags: tags + location: workspaceResourceGroup.location + hostPoolType: 'Pooled' + } +} + +module applicationGroup 'modules/applicationGroup.bicep' = { + scope: workspaceResourceGroup + name: replace(deploymentNamePrefix, '{rtype}', 'AVD-ApplicationGroup') + params: { + name: serviceResourceNameSuffix + tags: tags + location: workspaceResourceGroup.location + hostPoolId: hostPool.outputs.id + } +} + +module workspace 'modules/workspace.bicep' = { + scope: workspaceResourceGroup + name: replace(deploymentNamePrefix, '{rtype}', 'AVD-Workspace') + params: { + name: serviceResourceNameSuffix + tags: tags + location: workspaceResourceGroup.location + applicationGroupId: applicationGroup.outputs.id + } +} + +module sessionHost 'modules/sessionHost.bicep' = { + scope: workspaceResourceGroup + name: replace(deploymentNamePrefix, '{rtype}', 'AVD-SessionHosts') + params: { + name: serviceResourceNameSuffix + tags: tags + location: workspaceResourceGroup.location + localAdminName: localAdminName + localAdminPassword: localAdminPasswordGenerated + subnetName: 'ServicesSubnet' + vmSize: vmSize + vmCount: vmCount + vnetId: workspaceVirtualNetwork.id + hostPoolName: hostPool.outputs.name + hostPoolRegToken: hostPool.outputs.token + deploymentNameStructure: deploymentNamePrefix + } +} + +output connection_uri string = 'https://aka.ms/wvdarmweb' diff --git a/templates/workspace_services/avd-aad/bicep/modules/applicationGroup.bicep b/templates/workspace_services/avd-aad/bicep/modules/applicationGroup.bicep new file mode 100644 index 0000000000..affd3f5ed4 --- /dev/null +++ b/templates/workspace_services/avd-aad/bicep/modules/applicationGroup.bicep @@ -0,0 +1,16 @@ +param name string +param tags object +param location string +param hostPoolId string + +resource applicationGroup 'Microsoft.DesktopVirtualization/applicationGroups@2021-03-09-preview' = { + name: 'ag-${name}' + location: location + tags: tags + properties: { + applicationGroupType: 'Desktop' + hostPoolArmPath: hostPoolId + } +} + +output id string = applicationGroup.id diff --git a/templates/workspace_services/avd-aad/bicep/modules/hostPools.bicep b/templates/workspace_services/avd-aad/bicep/modules/hostPools.bicep new file mode 100644 index 0000000000..d44b389443 --- /dev/null +++ b/templates/workspace_services/avd-aad/bicep/modules/hostPools.bicep @@ -0,0 +1,36 @@ +param name string +param tags object +param location string +@allowed([ + 'Personal' + 'Pooled' +]) +param hostPoolType string + +param baseTime string = utcNow('u') + +var expirationTime = dateTimeAdd(baseTime, 'PT48H') + +resource hostPool 'Microsoft.DesktopVirtualization/hostPools@2021-03-09-preview' = { + name: 'hp-${name}' + location: location + tags: tags + properties: { + hostPoolType: hostPoolType + loadBalancerType: 'BreadthFirst' + preferredAppGroupType: 'Desktop' + maxSessionLimit: 999999 + startVMOnConnect: false + validationEnvironment: false + customRdpProperty: 'drivestoredirect:s:0;audiomode:i:0;videoplaybackmode:i:1;redirectclipboard:i:0;redirectprinters:i:0;devicestoredirect:s:0;redirectcomports:i:0;redirectsmartcards:i:1;usbdevicestoredirect:s:0;enablecredsspsupport:i:1;use multimon:i:1;targetisaadjoined:i:1' + registrationInfo: { + expirationTime: expirationTime + token: null + registrationTokenOperation: 'Update' + } + } +} + +output id string = hostPool.id +output name string = hostPool.name +output token string = string(hostPool.properties.registrationInfo.token) diff --git a/templates/workspace_services/avd-aad/bicep/modules/keyVaultSecret.bicep b/templates/workspace_services/avd-aad/bicep/modules/keyVaultSecret.bicep new file mode 100644 index 0000000000..1d35149291 --- /dev/null +++ b/templates/workspace_services/avd-aad/bicep/modules/keyVaultSecret.bicep @@ -0,0 +1,11 @@ +param workspaceKeyVaultName string +param secretName string +@secure() +param secretValue string + +resource localAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + name: '${workspaceKeyVaultName}/${secretName}' + properties: { + value: secretValue + } +} diff --git a/templates/workspace_services/avd-aad/bicep/modules/roleAssignment.bicep b/templates/workspace_services/avd-aad/bicep/modules/roleAssignment.bicep new file mode 100644 index 0000000000..a94034d513 --- /dev/null +++ b/templates/workspace_services/avd-aad/bicep/modules/roleAssignment.bicep @@ -0,0 +1,14 @@ +param name string + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = { + name: 'rbac${name}' + properties: { + roleDefinitionId: 'string' + principalId: 'string' + principalType: 'string' + description: 'string' + condition: 'string' + conditionVersion: 'string' + delegatedManagedIdentityResourceId: 'string' + } +} diff --git a/templates/workspace_services/avd-aad/bicep/modules/sessionHost.bicep b/templates/workspace_services/avd-aad/bicep/modules/sessionHost.bicep new file mode 100644 index 0000000000..fe4b58dad6 --- /dev/null +++ b/templates/workspace_services/avd-aad/bicep/modules/sessionHost.bicep @@ -0,0 +1,171 @@ +param name string +param tags object +param location string +param vnetId string +param subnetName string +param localAdminName string +@secure() +param localAdminPassword string +param vmSize string +param hostPoolRegToken string + +param deploymentNameStructure string = '${name}-${utcNow()}' +param intuneEnroll bool = false +param hostPoolName string +param vmCount int = 1 + +// All N-series except for NV_v4 use Nvidia +var installNVidiaGPUDriver = (startsWith(vmSize, 'Standard_N') && !(endsWith(vmSize, '_v4'))) ? true : false +// NV_v4 uses AMD +var installAmdGPUDriver = (startsWith(vmSize, 'Standard_NV') && endsWith(vmSize, '_v4')) ? true : false + +// Use the same VM templates as used by the Add VM to hostpool Portal +var nestedTemplatesLocation = 'https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/armtemplates/Hostpool_12-9-2021/nestedTemplates/' +var vmTemplateUri = '${nestedTemplatesLocation}managedDisks-galleryvm.json' + +var rdshPrefix = 'vm-${take(name, 10)}-' +var subnetId = '${vnetId}/subnets/${subnetName}' + +resource availabilitySet 'Microsoft.Compute/availabilitySets@2021-11-01' = { + name: 'avail-${name}' + location: location + properties: { + platformFaultDomainCount: 2 + platformUpdateDomainCount: 5 + } + sku: { + name: 'Aligned' + } + tags: tags +} + +// Deploy the session host VMs just like the Add VM to hostpool process would +resource vmDeployment 'Microsoft.Resources/deployments@2021-04-01' = { + name: replace(deploymentNameStructure, '{rtype}', 'AVD-VMs') + properties: { + mode: 'Incremental' + templateLink: { + uri: vmTemplateUri + contentVersion: '1.0.0.0' + } + parameters: { + artifactsLocation: { + value: 'https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/Configuration_02-23-2022.zip' + } + availabilityOption: { + value: 'AvailabilitySet' + } + availabilitySetName: { + value: availabilitySet.name + } + vmGalleryImageOffer: { + value: 'office-365' + } + vmGalleryImagePublisher: { + value: 'microsoftwindowsdesktop' + } + vmGalleryImageHasPlan: { + value: false + } + vmGalleryImageSKU: { + value: 'win11-21h2-avd-m365' + } + rdshPrefix: { + value: rdshPrefix + } + rdshNumberOfInstances: { + value: vmCount + } + rdshVMDiskType: { + value: 'StandardSSD_LRS' + } + rdshVmSize: { + value: vmSize + } + enableAcceleratedNetworking: { + value: true + } + vmAdministratorAccountUsername: { + value: localAdminName + } + vmAdministratorAccountPassword: { + value: localAdminPassword + } + // These values are required but unused for AAD join + administratorAccountUsername: { + value: '' + } + administratorAccountPassword: { + value: '' + } + // End required but unused for AAD join + 'subnet-id': { + value: subnetId + } + vhds: { + value: 'vhds/${rdshPrefix}' + } + location: { + value: location + } + createNetworkSecurityGroup: { + value: false + } + vmInitialNumber: { + value: 0 + } + hostpoolName: { + value: hostPoolName + } + hostpoolToken: { + value: hostPoolRegToken + } + aadJoin: { + value: true + } + intune: { + // In the CSE TRE DEMO tenant, Intune does not appear to be config'd + value: intuneEnroll + } + securityType: { + value: 'TrustedLaunch' + } + secureBoot: { + value: true + } + vTPM: { + value: true + } + vmImageVhdUri: { + value: '' + } + } + } +} + +resource sessionHostGPUDriver 'Microsoft.Compute/virtualMachines/extensions@2020-06-01' = [for i in range(0, vmCount): if (installNVidiaGPUDriver) { + name: '${rdshPrefix}${i}/InstallNvidiaGpuDriverWindows' + location: location + tags: tags + properties: { + publisher: 'Microsoft.HpcCompute' + type: 'NvidiaGpuDriverWindows' + typeHandlerVersion: '1.3' + autoUpgradeMinorVersion: true + } + dependsOn: [ + vmDeployment + ] +}] + +resource sessionHostAMDGPUDriver 'Microsoft.Compute/virtualMachines/extensions@2021-11-01' = [for i in range(0, vmCount): if (installAmdGPUDriver) { + name: '${rdshPrefix}${i}/AmdGpuDriverWindows' + location: location + tags: tags + properties: { + publisher: 'Microsoft.HpcCompute' + type: 'AmdGpuDriverWindows' + typeHandlerVersion: '1.0' + autoUpgradeMinorVersion: true + } +}] diff --git a/templates/workspace_services/avd-aad/bicep/modules/workspace.bicep b/templates/workspace_services/avd-aad/bicep/modules/workspace.bicep new file mode 100644 index 0000000000..62d2b18069 --- /dev/null +++ b/templates/workspace_services/avd-aad/bicep/modules/workspace.bicep @@ -0,0 +1,17 @@ +param name string +param tags object +param location string +param applicationGroupId string + +resource workspace 'Microsoft.DesktopVirtualization/workspaces@2021-03-09-preview' = { + name: 'ws-${name}' + tags: tags + location: location + properties: { + friendlyName: name + description: name + applicationGroupReferences: [ + applicationGroupId + ] + } +} diff --git a/templates/workspace_services/avd-aad/parameters.json b/templates/workspace_services/avd-aad/parameters.json new file mode 100644 index 0000000000..db94d670c0 --- /dev/null +++ b/templates/workspace_services/avd-aad/parameters.json @@ -0,0 +1,56 @@ +{ + "schemaVersion": "1.0.0-DRAFT+TODO", + "name": "avd-aad", + "created": "2021-06-03T11:54:54.0225968Z", + "modified": "2021-06-03T11:54:54.0225968Z", + "parameters": [ + { + "name": "id", + "source": { + "env": "ID" + } + }, + { + "name": "workspace_id", + "source": { + "env": "WORKSPACE_ID" + } + }, + { + "name": "tre_id", + "source": { + "env": "TRE_ID" + } + }, + { + "name": "azure_location", + "source": { + "env": "AZURE_LOCATION" + } + }, + { + "name": "localAdminName", + "source": { + "env": "LOCAL_ADMIN_NAME" + } + }, + { + "name": "vmSize", + "source": { + "env": "VM_SIZE" + } + }, + { + "name": "vmCount", + "source": { + "env": "VM_COUNT" + } + }, + { + "name": "vmLicenseType", + "source": { + "env": "VM_LICENSE_TYPE" + } + } + ] +} diff --git a/templates/workspace_services/avd-aad/porter.yaml b/templates/workspace_services/avd-aad/porter.yaml new file mode 100644 index 0000000000..17ca232e50 --- /dev/null +++ b/templates/workspace_services/avd-aad/porter.yaml @@ -0,0 +1,141 @@ +name: tre-service-avd-aad +version: 0.4.0 +description: "An Azure TRE service Azure Virtual Desktop" +registry: azuretre +dockerfile: Dockerfile.tmpl + +credentials: + - name: azure_tenant_id + env: ARM_TENANT_ID + - name: azure_subscription_id + env: ARM_SUBSCRIPTION_ID + - name: azure_client_id + env: ARM_CLIENT_ID + - name: azure_client_secret + env: ARM_CLIENT_SECRET + +parameters: + - name: workspace_id + type: string + - name: tre_id + type: string + - name: id + type: string + - name: azure_location + type: string + - name: localAdminName + type: string + default: adminuser + - name: vmSize + type: string + default: "Standard_DS2as_v4" + - name: vmCount + type: integer + default: 1 + - name: vmLicenseType + type: string + default: "Windows_Client" + +outputs: + - name: connection_uri + type: string + default: "https://aka.ms/wvdarmweb" + applyTo: + - install + +mixins: + - exec + - az: + extensions: + - azure-firewall + +install: + - az: + description: "az login" + arguments: + - login + flags: + service-principal: + username: "{{ bundle.credentials.azure_client_id}}" + password: "{{ bundle.credentials.azure_client_secret}}" + tenant: "{{ bundle.credentials.azure_tenant_id}}" + - az: + description: "Set Az FW rules for AVD - TODO: use David's solution" + arguments: + - network + - firewall + - network-rule + - create + flags: + # TODO: Use WS ID as a param + collection-name: "nrc-mrtredev-ws-0821-svc-f62b" + priority: 1000 + destination-ports: "443" + # TODO: Do not hardcode + firewall-name: "fw-mrtredev" + name: "AVDInfra" + protocols: "TCP" + action: "Allow" + description: "'Allow access to AVD infrastructure'" + # Uses Service Tag + dest-addr: "WindowsVirtualDesktop" + # TODO: Do not hardcode + source-addresses: "10.0.9.0/25" + resource-group: "rg-mrtredev" + - az: + description: "Set Az FW rules for AVD - TODO: use David's solution" + arguments: + - network + - firewall + - network-rule + - create + flags: + # TODO: Use WS ID as a param + collection-name: "nrc-mrtredev-ws-0821-svc-f62b" + destination-ports: "443" + # TODO: Do not hardcode + firewall-name: "fw-mrtredev" + name: "AVD_PS_DSC_Modules" + protocols: "TCP" + description: "'Allow access to PowerShell DSC Modules'" + # TODO: Do not hardcode + source-addresses: "10.0.9.0/25" + destination-fqdns: "'wvdportalstorageblob.blob.core.windows.net'" + resource-group: "rg-mrtredev" + - az: + description: "Deploy Azure Virtual Desktop Service" + name: "tre-service-avd-aad" + arguments: + - deployment + - sub + - create + flags: + template-file: main.json + location: "{{ bundle.parameters.azure_location }}" + name: "{{ bundle.parameters.id }}" + parameters: id={{ bundle.parameters.id }} workspaceId={{ bundle.parameters.workspace_id }} treId={{ bundle.parameters.tre_id }} localAdminName={{ bundle.parameters.localAdminName }} vmSize={{ bundle.parameters.vmSize }} vmCount={{ bundle.parameters.vmCount }} + outputs: + # TODO: Porter doesn't see this + - name: connection_uri + jsonPath: "$.properties.outputs['connection_uri'].value" + - az: + description: "Remove Az FW rule only needed during deployment" + arguments: + - network + - firewall + - network-rule + - delete + flags: + # TODO: Use WS ID as a param + collection-name: "nrc-mrtredev-ws-0821-svc-f62b" + # TODO: Do not hardcode + firewall-name: "fw-mrtredev" + name: "AVD_PS_DSC_Modules" + resource-group: "rg-mrtredev" + +uninstall: + - exec: + description: "Uninstall Azure Virtual Desktop Service" + command: echo + arguments: + - "This service does not yet implement the uninstall action." diff --git a/templates/workspace_services/avd-aad/template_schema.json b/templates/workspace_services/avd-aad/template_schema.json new file mode 100644 index 0000000000..a8ce71553f --- /dev/null +++ b/templates/workspace_services/avd-aad/template_schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://github.com/microsoft/AzureTRE/templates/workspace_services/avd-aad/template_schema.json", + "type": "object", + "title": "Azure Virtual Desktop", + "description": "Azure Virtual Desktop hostpool joined to Azure Active Directory", + "required": [], + "properties": { + "localAdminName": { + "type": "string", + "title": "Local Admin Name", + "description": "The name of the local administrator account to be created on the virtual machine.", + "default": "adminuser" + }, + "vmSize": { + "type": "string", + "title": "Virtual Machine Size", + "description": "The size of the virtual machine to be created.", + "enum": [ + "Standard_D2as_v4", + "Standard_D4as_v4", + "Standard_D8as_v4", + "Standard_D16as_v4" + ], + "default": "Standard_D2as_v4" + }, + "vmCount": { + "type": "integer", + "title": "Number of Virtual Machines", + "description": "The number of virtual machines to be created.", + "default": 1 + }, + "vmLicenseType": { + "type": "string", + "title": "Virtual Machine License Type", + "description": "The license type of the virtual machine to be created.", + "enum": [ + "Windows_Server", + "Windows_Client" + ], + "default": "Windows_Client" + } + } +}