diff --git a/.github/workflows/ms.compute.images.yml b/.github/workflows/ms.compute.images.yml index 93b6897198..82ff8ca90a 100644 --- a/.github/workflows/ms.compute.images.yml +++ b/.github/workflows/ms.compute.images.yml @@ -106,8 +106,7 @@ jobs: - name: 'Using test file [${{ matrix.moduleTestFilePaths }}]' uses: ./.github/actions/templates/validateModuleDeployment with: - templateFilePath: '${{ env.modulePath }}/deploy.bicep' - parameterFilePath: '${{ env.modulePath }}/${{ matrix.moduleTestFilePaths }}' + templateFilePath: '${{ env.modulePath }}/${{ matrix.moduleTestFilePaths }}' location: '${{ env.location }}' resourceGroupName: '${{ env.resourceGroupName }}' subscriptionId: '${{ secrets.ARM_SUBSCRIPTION_ID }}' diff --git a/modules/Microsoft.Compute/images/.test/.scripts/Copy-VhdToStorageAccount.ps1 b/modules/Microsoft.Compute/images/.test/.scripts/Copy-VhdToStorageAccount.ps1 new file mode 100644 index 0000000000..ff8568b0a9 --- /dev/null +++ b/modules/Microsoft.Compute/images/.test/.scripts/Copy-VhdToStorageAccount.ps1 @@ -0,0 +1,124 @@ +<# + .SYNOPSIS + Copy a VHD baked from a given image template to a given destination storage account blob container + + .DESCRIPTION + Copy a VHD baked from a given image template to a given destination storage account blob container + + .PARAMETER ImageTemplateName + Mandatory. The name of the image template + + .PARAMETER ImageTemplateResourceGroup + Mandatory. The resource group name of the image template + + .PARAMETER DestinationStorageAccountName + Mandatory. The name of the destination storage account + + .PARAMETER DestinationContainerName + Optional. The name of the existing destination blob container + + .PARAMETER VhdName + Optional. Specify a different name for the destination VHD file + + .PARAMETER WaitForComplete + Optional. Run the command synchronously. Wait for the completion of the copy. + + .EXAMPLE + Copy-VhdToStorageAccount -ImageTemplateName 'vhd-img-template-001-2022-07-29-15-54-01' -ImageTemplateResourceGroup 'validation-rg' -DestinationStorageAccountName 'vhdstorage001' + + Copy a VHD created by image template 'vhd-img-template-001-2022-07-29-15-54-01' in resource group 'validation-rg' to destination storage account 'vhdstorage001' in blob container named 'vhds'. Save the VHD file as 'vhd-img-template-001-2022-07-29-15-54-01.vhd'. + + .EXAMPLE + Copy-VhdToStorageAccount -ImageTemplateName 'vhd-img-template-001-2022-07-29-15-54-01' -ImageTemplateResourceGroup 'validation-rg' -DestinationStorageAccountName 'vhdstorage001' -VhdName 'vhd-img-template-001' -WaitForComplete + + Copy a VHD baked by image template 'vhd-img-template-001-2022-07-29-15-54-01' in resource group 'validation-rg' to destination storage account 'vhdstorage001' in a blob container named 'vhds' and wait for the completion of the copy. Save the VHD file as 'vhd-img-template-001.vhd'. +#> + +[CmdletBinding(SupportsShouldProcess)] +param ( + [Parameter(Mandatory = $true)] + [string] $ImageTemplateName, + + [Parameter(Mandatory = $true)] + [string] $ImageTemplateResourceGroup, + + [Parameter(Mandatory = $true)] + [string] $DestinationStorageAccountName, + + [Parameter(Mandatory = $false)] + [string] $DestinationContainerName = 'vhds', + + [Parameter(Mandatory = $false)] + [string] $VhdName = $ImageTemplateName, + + [Parameter(Mandatory = $false)] + [switch] $WaitForComplete +) + +begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + + # Install required modules + $currentVerbosePreference = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + $requiredModules = @( + 'Az.ImageBuilder', + 'Az.Storage' + ) + foreach ($moduleName in $requiredModules) { + if (-not ($installedModule = Get-Module $moduleName -ListAvailable)) { + Install-Module $moduleName -Repository 'PSGallery' -Force -Scope 'CurrentUser' + if ($installed = Get-Module -Name $moduleName -ListAvailable) { + Write-Verbose ('Installed module [{0}] with version [{1}]' -f $installed.Name, $installed.Version) -Verbose + } + } else { + Write-Verbose ('Module [{0}] already installed in version [{1}]' -f $installedModule[0].Name, $installedModule[0].Version) -Verbose + } + } + $VerbosePreference = $currentVerbosePreference +} + +process { + # Retrieving and initializing parameters before the blob copy + Write-Verbose 'Initializing source storage account parameters before the blob copy' -Verbose + Write-Verbose ('Retrieving source storage account from image template [{0}] in resource group [{1}]' -f $imageTemplateName, $imageTemplateResourceGroup) -Verbose + Get-InstalledModule + $imgtRunOutput = Get-AzImageBuilderTemplateRunOutput -ImageTemplateName $imageTemplateName -ResourceGroupName $imageTemplateResourceGroup | Where-Object ArtifactUri -NE $null + $sourceUri = $imgtRunOutput.ArtifactUri + $sourceStorageAccountName = $sourceUri.Split('//')[1].Split('.')[0] + $storageAccountList = Get-AzStorageAccount + $sourceStorageAccount = $storageAccountList | Where-Object StorageAccountName -EQ $sourceStorageAccountName + $sourceStorageAccountContext = $sourceStorageAccount.Context + $sourceStorageAccountRGName = $sourceStorageAccount.ResourceGroupName + Write-Verbose ('Retrieving artifact uri [{0}] stored in resource group [{1}]' -f $sourceUri, $sourceStorageAccountRGName) -Verbose + + Write-Verbose 'Initializing destination storage account parameters before the blob copy' -Verbose + $destinationStorageAccount = $storageAccountList | Where-Object StorageAccountName -EQ $destinationStorageAccountName + $destinationStorageAccountContext = $destinationStorageAccount.Context + $destinationBlobName = "$vhdName.vhd" + Write-Verbose ('Planning for destination blob name [{0}] in container [{1}] and storage account [{2}]' -f $destinationBlobName, $destinationContainerName, $destinationStorageAccountName) -Verbose + + # Copying the VHD to a destination blob container + $resourceActionInputObject = @{ + AbsoluteUri = $sourceUri + Context = $sourceStorageAccountContext + DestContext = $destinationStorageAccountContext + DestBlob = $destinationBlobName + DestContainer = $destinationContainerName + Force = $true + } + + if ($PSCmdlet.ShouldProcess('Storage blob copy of VHD [{0}]' -f $destinationBlobName, 'Start')) { + $destBlob = Start-AzStorageBlobCopy @resourceActionInputObject + Write-Verbose ('Copied/initialized copy of VHD from URI [{0}] to container [{1}] in storage account [{2}]' -f $sourceUri, $destinationContainerName, $destinationStorageAccountName) -Verbose + } + + if ($WaitForComplete) { + $destBlob | Get-AzStorageBlobCopyState -WaitForComplete + } +} + +end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) +} + diff --git a/modules/Microsoft.Compute/images/.test/.scripts/Start-ImageTemplate.ps1 b/modules/Microsoft.Compute/images/.test/.scripts/Start-ImageTemplate.ps1 new file mode 100644 index 0000000000..9118832ea3 --- /dev/null +++ b/modules/Microsoft.Compute/images/.test/.scripts/Start-ImageTemplate.ps1 @@ -0,0 +1,79 @@ +<# + .SYNOPSIS + Create image artifacts from a given image template + + .DESCRIPTION + Create image artifacts from a given image template + + .PARAMETER ImageTemplateName + Mandatory. The name of the image template + + .PARAMETER ImageTemplateResourceGroup + Mandatory. The resource group name of the image template + + .PARAMETER NoWait + Optional. Run the command asynchronously + + .EXAMPLE + Start-AzImageBuilderTemplate -ImageTemplateName 'vhd-img-template-001-2022-07-29-15-54-01' -ImageTemplateResourceGroup 'validation-rg' + + Create image artifacts from image template 'vhd-img-template-001-2022-07-29-15-54-01' in resource group 'validation-rg' and wait for their completion + + .EXAMPLE + Start-AzImageBuilderTemplate -ImageTemplateName 'vhd-img-template-001-2022-07-29-15-54-01' -ImageTemplateResourceGroup 'validation-rg' -NoWait + + Start the creation of artifacts from image template 'vhd-img-template-001-2022-07-29-15-54-01' in resource group 'validation-rg' and do not wait for their completion +#> + +[CmdletBinding(SupportsShouldProcess)] +param ( + [Parameter(Mandatory = $true)] + [string] $ImageTemplateName, + + [Parameter(Mandatory = $true)] + [string] $ImageTemplateResourceGroup, + + [Parameter(Mandatory = $false)] + [switch] $NoWait +) + +begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + + # Install required modules + $currentVerbosePreference = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + $requiredModules = @( + 'Az.ImageBuilder' + ) + foreach ($moduleName in $requiredModules) { + if (-not ($installedModule = Get-Module $moduleName -ListAvailable)) { + Install-Module $moduleName -Repository 'PSGallery' -Force -Scope 'CurrentUser' + if ($installed = Get-Module -Name $moduleName -ListAvailable) { + Write-Verbose ('Installed module [{0}] with version [{1}]' -f $installed.Name, $installed.Version) -Verbose + } + } else { + Write-Verbose ('Module [{0}] already installed in version [{1}]' -f $installedModule[0].Name, $installedModule[0].Version) -Verbose + } + } + $VerbosePreference = $currentVerbosePreference +} + +process { + # Create image artifacts from existing image template + $resourceActionInputObject = @{ + ImageTemplateName = $imageTemplateName + ResourceGroupName = $imageTemplateResourceGroup + } + if ($NoWait) { + $resourceActionInputObject['NoWait'] = $true + } + if ($PSCmdlet.ShouldProcess('Image template [{0}]' -f $imageTemplateName, 'Start')) { + $null = Start-AzImageBuilderTemplate @resourceActionInputObject + Write-Verbose ('Created/initialized creation of image artifacts from image template [{0}] in resource group [{1}]' -f $imageTemplateName, $imageTemplateResourceGroup) -Verbose + } +} + +end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) +} diff --git a/modules/Microsoft.Compute/images/.test/common/dependencies.bicep b/modules/Microsoft.Compute/images/.test/common/dependencies.bicep new file mode 100644 index 0000000000..d422b9c273 --- /dev/null +++ b/modules/Microsoft.Compute/images/.test/common/dependencies.bicep @@ -0,0 +1,149 @@ +@description('Optional. The location to deploy to.') +param location string = resourceGroup().location + +@description('Required. The name of the Managed Identity to create.') +param managedIdentityName string + +@description('Required. The name of the Storage Account to create and to copy the VHD into.') +param storageAccountName string + +@description('Required. The name prefix of the Image Template to create.') +param imageTemplateNamePrefix string + +@description('Generated. Do not provide a value! This date value is used to generate a unique image template name.') +param baseTime string = utcNow('yyyy-MM-dd-HH-mm-ss') + +@description('Required. The name of the Deployment Script to create for triggering the image creation.') +param triggerImageDeploymentScriptName string + +@description('Required. The name of the Deployment Script to copy the VHD to a destination storage account.') +param copyVhdDeploymentScriptName string + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { + name: managedIdentityName + location: location +} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' = { + name: storageAccountName + location: location + kind: 'StorageV2' + sku: { + name: 'Standard_LRS' + } + properties: { + allowBlobPublicAccess: false + } + resource blobServices 'blobServices@2021-09-01' = { + name: 'default' + resource container 'containers@2021-09-01' = { + name: 'vhds' + properties: { + publicAccess: 'None' + } + } + } +} + +module roleAssignment 'dependencies_rbac.bicep' = { + name: '${deployment().name}-MSI-roleAssignment' + scope: subscription() + params: { + managedIdentityPrincipalId: managedIdentity.properties.principalId + managedIdentityResourceId: managedIdentity.id + } +} + +// Deploy image template +resource imageTemplate 'Microsoft.VirtualMachineImages/imageTemplates@2022-02-14' = { + name: '${imageTemplateNamePrefix}-${baseTime}' + location: location + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentity.id}': {} + } + } + properties: { + buildTimeoutInMinutes: 0 + vmProfile: { + vmSize: 'Standard_D2s_v3' + osDiskSizeGB: 127 + } + source: { + type: 'PlatformImage' + publisher: 'MicrosoftWindowsDesktop' + offer: 'Windows-10' + sku: '19h2-evd' + version: 'latest' + } + distribute: [ + { + type: 'VHD' + runOutputName: '${imageTemplateNamePrefix}-VHD' + artifactTags: {} + } + ] + customize: [ + { + restartTimeout: '30m' + type: 'WindowsRestart' + } + ] + } +} + +// Trigger VHD creation +resource triggerImageDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + name: triggerImageDeploymentScriptName + location: location + kind: 'AzurePowerShell' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentity.id}': {} + } + } + properties: { + azPowerShellVersion: '8.0' + retentionInterval: 'P1D' + arguments: '-ImageTemplateName \\"${imageTemplate.name}\\" -ImageTemplateResourceGroup \\"${resourceGroup().name}\\"' + scriptContent: loadTextContent('../.scripts/Start-ImageTemplate.ps1') + cleanupPreference: 'OnSuccess' + forceUpdateTag: baseTime + } + dependsOn: [ + roleAssignment + ] +} + +// Copy VHD to destination storage account +resource copyVhdDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + name: copyVhdDeploymentScriptName + location: location + kind: 'AzurePowerShell' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentity.id}': {} + } + } + properties: { + azPowerShellVersion: '8.0' + retentionInterval: 'P1D' + arguments: '-ImageTemplateName \\"${imageTemplate.name}\\" -ImageTemplateResourceGroup \\"${resourceGroup().name}\\" -DestinationStorageAccountName \\"${storageAccount.name}\\" -VhdName \\"${imageTemplateNamePrefix}\\" -WaitForComplete' + scriptContent: loadTextContent('../.scripts/Copy-VhdToStorageAccount.ps1') + cleanupPreference: 'OnSuccess' + forceUpdateTag: baseTime + } + dependsOn: [ triggerImageDeploymentScript ] +} + +@description('The URI of the created VHD.') +output vhdUri string = 'https://${storageAccount.name}.blob.core.windows.net/vhds/${imageTemplateNamePrefix}.vhd' + +@description('The principal ID of the created Managed Identity.') +output managedIdentityPrincipalId string = managedIdentity.properties.principalId + +@description('The resource ID of the created Managed Identity.') +output managedIdentityResourceId string = managedIdentity.id diff --git a/modules/Microsoft.Compute/images/.test/common/dependencies_rbac.bicep b/modules/Microsoft.Compute/images/.test/common/dependencies_rbac.bicep new file mode 100644 index 0000000000..cdca1b63bd --- /dev/null +++ b/modules/Microsoft.Compute/images/.test/common/dependencies_rbac.bicep @@ -0,0 +1,16 @@ +targetScope = 'subscription' + +@description('Required. The resource ID of the created Managed Identity.') +param managedIdentityResourceId string + +@description('Required. The principal ID of the created Managed Identity.') +param managedIdentityPrincipalId string + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(subscription().subscriptionId, 'Contributor', managedIdentityResourceId) + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') // Contributor + principalId: managedIdentityPrincipalId + principalType: 'ServicePrincipal' + } +} diff --git a/modules/Microsoft.Compute/images/.test/common/deploy.test.bicep b/modules/Microsoft.Compute/images/.test/common/deploy.test.bicep new file mode 100644 index 0000000000..6cc6933050 --- /dev/null +++ b/modules/Microsoft.Compute/images/.test/common/deploy.test.bicep @@ -0,0 +1,63 @@ +targetScope = 'subscription' + +// ========== // +// Parameters // +// ========== // + +@description('Optional. The name of the resource group to deploy for a testing purposes') +@maxLength(90) +param resourceGroupName string = 'ms.compute.images-${serviceShort}-rg' + +@description('Optional. The location to deploy resources to') +param location string = deployment().location + +@description('Optional. A short identifier for the kind of deployment. Should be kept short to not run into resource-name length-constraints') +param serviceShort string = 'cicom' + +// =========== // +// Deployments // +// =========== // + +// General resources +// ================= +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: resourceGroupName + location: location +} + +module resourceGroupResources 'dependencies.bicep' = { + scope: resourceGroup + name: '${uniqueString(deployment().name, location)}-paramNested' + params: { + managedIdentityName: 'dep-<>-msi-${serviceShort}' + storageAccountName: 'dep<>sa${serviceShort}01' + imageTemplateNamePrefix: 'dep-<>-imgt-${serviceShort}' + triggerImageDeploymentScriptName: 'dep-<>-ds-${serviceShort}-triggerImageTemplate' + copyVhdDeploymentScriptName: 'dep-<>-ds-${serviceShort}-copyVhdToStorage' + } +} + +// ============== // +// Test Execution // +// ============== // +module testDeployment '../../deploy.bicep' = { + scope: resourceGroup + name: '${uniqueString(deployment().name, location)}-test-${serviceShort}' + params: { + name: '<>${serviceShort}001' + osAccountType: 'Premium_LRS' + osDiskBlobUri: resourceGroupResources.outputs.vhdUri + osDiskCaching: 'ReadWrite' + osType: 'Windows' + hyperVGeneration: 'V1' + roleAssignments: [ + { + principalIds: [ + resourceGroupResources.outputs.managedIdentityPrincipalId + ] + roleDefinitionIdOrName: 'Reader' + } + ] + zoneResilient: true + } +} diff --git a/modules/Microsoft.Compute/images/.test/parameters.json b/modules/Microsoft.Compute/images/.test/parameters.json deleted file mode 100644 index fed467631c..0000000000 --- a/modules/Microsoft.Compute/images/.test/parameters.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "name": { - "value": "<>-az-img-x-001" - }, - "osAccountType": { - "value": "Premium_LRS" - }, - "osType": { - "value": "Windows" - }, - "osDiskBlobUri": { - "value": "https://adp<>azsavhd001.blob.core.windows.net/vhds/adp-<>-az-imgt-vhd-001.vhd" - }, - "osDiskCaching": { - "value": "ReadWrite" - }, - "zoneResilient": { - "value": true - }, - "hyperVGeneration": { - "value": "V1" - }, - "roleAssignments": { - "value": [ - { - "roleDefinitionIdOrName": "Reader", - "principalIds": [ - "<>" - ] - } - ] - } - } -} diff --git a/modules/Microsoft.Compute/images/readme.md b/modules/Microsoft.Compute/images/readme.md index 135c138519..7dbfe27c76 100644 --- a/modules/Microsoft.Compute/images/readme.md +++ b/modules/Microsoft.Compute/images/readme.md @@ -161,7 +161,7 @@ The following module usage examples are retrieved from the content of the files >**Note**: Each example lists all the required parameters first, followed by the rest - each in alphabetical order. -

Example 1: Parameters

+

Example 1: Common

@@ -169,12 +169,12 @@ The following module usage examples are retrieved from the content of the files ```bicep module images './Microsoft.Compute/images/deploy.bicep' = { - name: '${uniqueString(deployment().name)}-Images' + name: '${uniqueString(deployment().name, location)}-test-cicom' params: { // Required parameters - name: '<>-az-img-x-001' + name: '<>cicom001' osAccountType: 'Premium_LRS' - osDiskBlobUri: 'https://adp<>azsavhd001.blob.core.windows.net/vhds/adp-<>-az-imgt-vhd-001.vhd' + osDiskBlobUri: '' osDiskCaching: 'ReadWrite' osType: 'Windows' // Non-required parameters @@ -182,7 +182,7 @@ module images './Microsoft.Compute/images/deploy.bicep' = { roleAssignments: [ { principalIds: [ - '<>' + '' ] roleDefinitionIdOrName: 'Reader' } @@ -206,13 +206,13 @@ module images './Microsoft.Compute/images/deploy.bicep' = { "parameters": { // Required parameters "name": { - "value": "<>-az-img-x-001" + "value": "<>cicom001" }, "osAccountType": { "value": "Premium_LRS" }, "osDiskBlobUri": { - "value": "https://adp<>azsavhd001.blob.core.windows.net/vhds/adp-<>-az-imgt-vhd-001.vhd" + "value": "" }, "osDiskCaching": { "value": "ReadWrite" @@ -228,7 +228,7 @@ module images './Microsoft.Compute/images/deploy.bicep' = { "value": [ { "principalIds": [ - "<>" + "" ], "roleDefinitionIdOrName": "Reader" }