From eb69c10f6e65daef5029a8a144834943d3348f71 Mon Sep 17 00:00:00 2001 From: Sven Aelterman <17446043+SvenAelterman@users.noreply.github.com> Date: Thu, 15 Aug 2024 19:48:29 -0500 Subject: [PATCH] Support for deploying image build resources in different subscription Register ContainerInstance in subscription for image building --- research-hub/deploy.ps1 | 21 +- .../hub-modules/imaging/aib-resources.bicep | 195 ++++++++++++++++++ research-hub/hub-modules/imaging/main.bicep | 184 ++--------------- research-hub/main.bicep | 20 +- .../Modules/AzSubscriptionManagement.psm1 | 2 - 5 files changed, 243 insertions(+), 179 deletions(-) create mode 100644 research-hub/hub-modules/imaging/aib-resources.bicep diff --git a/research-hub/deploy.ps1 b/research-hub/deploy.ps1 index b0a5d53..a96c04f 100644 --- a/research-hub/deploy.ps1 +++ b/research-hub/deploy.ps1 @@ -47,22 +47,35 @@ param ( Location = $Location } +Write-Verbose "Using template parameter file '$TemplateParameterFile'" +[string]$TemplateParameterJsonFile = [System.IO.Path]::ChangeExtension($TemplateParameterFile, 'json') +bicep build-params $TemplateParameterFile --outfile $TemplateParameterJsonFile + +# Read the values from the parameters file, to use when generating the $DeploymentName value +$ParameterFileContents = (Get-Content $TemplateParameterJsonFile | ConvertFrom-Json) +$WorkloadName = $ParameterFileContents.parameters.workloadName.value +$ImagingSubscriptionId = $ParameterFileContents.parameters.imageBuildSubscriptionId.value + # Import the Azure subscription management module Import-Module ..\scripts\PowerShell\Modules\AzSubscriptionManagement.psm1 +# Determine if a cloud context switch is required for configuring the image build subscription, which could be different from the hub subscription +Set-AzContextWrapper -SubscriptionId $ImagingSubscriptionId -Environment $Environment + +# LATER: Run provider and feature registrations in parallel +Register-AzResourceProviderWrapper -ProviderNamespace "Microsoft.Storage" +Register-AzResourceProviderWrapper -ProviderNamespace "Microsoft.ContainerInstance" # For image builder + # Determine if a cloud context switch is required Set-AzContextWrapper -SubscriptionId $TargetSubscriptionId -Environment $Environment # Ensure the EncryptionAtHost feature is registered for the current subscription # LATER: Do this with a deployment script in Bicep Register-AzProviderFeatureWrapper -ProviderNamespace "Microsoft.Compute" -FeatureName "EncryptionAtHost" -# LATER: Run provider and feature registrations in parallel -Register-AzResourceProviderWrapper -ProviderNamespace "Microsoft.Storage" - # Remove the module from the session (always, even in WhatIf mode) Remove-Module AzSubscriptionManagement -WhatIf:$false -[string]$DeploymentName = "ResearchHub-$(Get-Date -Format 'yyyyMMddThhmmssZ' -AsUTC)" +[string]$DeploymentName = "$WorkloadName-$(Get-Date -Format 'yyyyMMddThhmmssZ' -AsUTC)" $CmdLetParameters.Add('Name', $DeploymentName) $DeploymentResults = New-AzDeployment @CmdLetParameters diff --git a/research-hub/hub-modules/imaging/aib-resources.bicep b/research-hub/hub-modules/imaging/aib-resources.bicep new file mode 100644 index 0000000..3635b0e --- /dev/null +++ b/research-hub/hub-modules/imaging/aib-resources.bicep @@ -0,0 +1,195 @@ +param location string = resourceGroup().location +param tags object +param deploymentNameStructure string +param environment string +param namingConvention string +param sequence int +param workloadName string +param imageReference object +param namingStructure string + +param enableAvmTelemetry bool +param sampleImageName string + +var customRoleName = 'Azure Image Builder Service Image Creation' +var customRoleGuid = guid(resourceGroup().id, customRoleName) + +// Create a custom role that's allowed to create images +resource aibRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' = { + name: customRoleGuid + properties: { + roleName: '${customRoleName} (${customRoleGuid})' + description: 'Image Builder access to create resources for the image build' + assignableScopes: [resourceGroup().id] + permissions: [ + { + actions: [ + 'Microsoft.Compute/galleries/read' + 'Microsoft.Compute/galleries/images/read' + 'Microsoft.Compute/galleries/images/versions/read' + 'Microsoft.Compute/galleries/images/versions/write' + + 'Microsoft.Compute/images/write' + 'Microsoft.Compute/images/read' + 'Microsoft.Compute/images/delete' + + 'Microsoft.Network/virtualNetworks/read' + 'Microsoft.Network/virtualNetworks/subnets/join/action' + ] + } + ] + } +} + +// Create dedicated UAMI for image building +module aibUamiModule '../../../shared-modules/security/uami.bicep' = { + name: take(replace(deploymentNameStructure, '{rtype}', 'uami-aib'), 64) + params: { + location: location + tags: tags + uamiName: replace(namingStructure, '{rtype}', 'uami-aib') + } +} + +// Assign the new custom role to the UAMI +module uamiImagingRoleAssignmentModule '../../../module-library/roleAssignments/roleAssignment-rg.bicep' = { + name: take(replace(deploymentNameStructure, '{rtype}', 'uami-img-rbac'), 64) + params: { + principalId: aibUamiModule.outputs.principalId + roleDefinitionId: aibRoleDefinition.id + principalType: 'ServicePrincipal' + } +} + +var aibNetworkAddressPrefix = '192.168.1.0/24' + +module aibNetworkModule '../../../shared-modules/networking/main.bicep' = { + name: take(replace(deploymentNameStructure, '{rtype}', 'network-aib'), 64) + params: { + deploymentNameStructure: deploymentNameStructure + location: location + namingStructure: namingStructure + vnetAddressPrefixes: [aibNetworkAddressPrefix] + + subnetDefs: { + ImageBuilderSubnet: { + addressPrefix: cidrSubnet(aibNetworkAddressPrefix, 24, 0) + privateLinkServiceNetworkPolicies: 'Disabled' + // For compliance, all subnets need a NSG + securityRules: [] + } + } + + tags: tags + } +} + +module computeGalleryNameModule '../../../module-library/createValidAzResourceName.bicep' = { + name: take(replace(deploymentNameStructure, '{rtype}', 'galname'), 64) + params: { + environment: environment + location: location + namingConvention: namingConvention + resourceType: 'gal' + sequence: sequence + workloadName: workloadName + subWorkloadName: '' + } + dependsOn: [uamiImagingRoleAssignmentModule] +} + +module computeGalleryModule 'br/public:avm/res/compute/gallery:0.3.1' = { + name: take(replace(deploymentNameStructure, '{rtype}', 'gal'), 64) + params: { + name: computeGalleryNameModule.outputs.validName + location: location + + images: [ + { + hyperVGeneration: 'V2' + name: sampleImageName + + // TODO: Customize based on Image Reference + offer: 'WindowsClient' + osType: 'Windows' + publisher: 'Customer' + sku: 'Windows-11-Enterprise-23H2-Gen2' + + securityType: 'TrustedLaunch' + isAcceleratedNetworkSupported: true + isHibernateSupported: true + osState: 'Generalized' + + // Avoid warnings when using the image from the GUI + maxRecommendedMemory: 4000 + maxRecommendedvCPUs: 128 + minRecommendedMemory: 4 + minRecommendedvCPUs: 2 + + tags: tags + } + ] + + tags: tags + enableTelemetry: enableAvmTelemetry + } +} + +module imageTemplateModule 'br/public:avm/res/virtual-machine-images/image-template:0.1.1' = { + name: take(replace(deploymentNameStructure, '{rtype}', 'img'), 64) + params: { + name: replace(namingStructure, '{rtype}', 'img') + location: location + imageSource: union(imageReference, { type: 'PlatformImage' }) + + vmSize: 'Standard_D2as_v5' + + // TODO: Load from customizable file + customizationSteps: [ + { + type: 'WindowsUpdate' + filters: [ + 'exclude:$_.Title -like \'*Preview*\'' + 'include:$true' + ] + } + { + type: 'PowerShell' + name: 'Install Microsoft Storage Explorer' + runElevated: true + runAsSystem: true + scriptUri: 'https://raw.githubusercontent.com/SvenAelterman/Azure-HubAndSpokeResearchEnclave/main/scripts/PowerShell/Scripts/AIB/Windows/Install-StorageExplorer.ps1' + sha256Checksum: 'a8122168d9700c8e3b2fe03804e181a88fdc4833bbeee19bd42e58e3d85903c5' + } + { + type: 'PowerShell' + name: 'Install azcopy' + runElevated: true + runAsSystem: true + scriptUri: 'https://raw.githubusercontent.com/SvenAelterman/Azure-HubAndSpokeResearchEnclave/main/scripts/PowerShell/Scripts/AIB/Windows/Install-AzCopy.ps1' + sha256Checksum: '45453a42a0d8d75f4aecb0e83566078373b3320489431b158f8ea4ae08379e59' + } + ] + + distributions: [ + { + type: 'SharedImage' + sharedImageGalleryImageDefinitionResourceId: '${computeGalleryModule.outputs.resourceId}/images/${sampleImageName}' + excludeFromLatest: false + } + ] + + managedIdentities: { + userAssignedResourceIds: [ + aibUamiModule.outputs.id + ] + } + + subnetResourceId: aibNetworkModule.outputs.createdSubnets.ImageBuilderSubnet.id + + enableTelemetry: enableAvmTelemetry + tags: tags + } +} + +output imageDefinitionId string = imageTemplateModule.outputs.resourceId diff --git a/research-hub/hub-modules/imaging/main.bicep b/research-hub/hub-modules/imaging/main.bicep index 0ad6e23..37edba9 100644 --- a/research-hub/hub-modules/imaging/main.bicep +++ b/research-hub/hub-modules/imaging/main.bicep @@ -1,4 +1,7 @@ -param location string = resourceGroup().location +targetScope = 'subscription' + +param resourceGroupName string +param location string param tags object param deploymentNameStructure string param environment string @@ -11,181 +14,32 @@ param namingStructure string param enableAvmTelemetry bool = true param sampleImageName string = 'sample' -var customRoleName = 'Azure Image Builder Service Image Creation' -var customRoleGuid = guid(resourceGroup().id, customRoleName) - -// Create a custom role that's allowed to create images -resource aibRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' = { - name: customRoleGuid - properties: { - roleName: '${customRoleName} (${customRoleGuid})' - description: 'Image Builder access to create resources for the image build' - assignableScopes: [resourceGroup().id] - permissions: [ - { - actions: [ - 'Microsoft.Compute/galleries/read' - 'Microsoft.Compute/galleries/images/read' - 'Microsoft.Compute/galleries/images/versions/read' - 'Microsoft.Compute/galleries/images/versions/write' - - 'Microsoft.Compute/images/write' - 'Microsoft.Compute/images/read' - 'Microsoft.Compute/images/delete' - - 'Microsoft.Network/virtualNetworks/read' - 'Microsoft.Network/virtualNetworks/subnets/join/action' - ] - } - ] - } +// Create a resource group +resource imagingRg 'Microsoft.Resources/resourceGroups@2023-07-01' = { + #disable-next-line BCP334 + name: resourceGroupName + location: location + tags: tags } -// TODO: Create dedicated UAMI for image building -module aibUamiModule '../../../shared-modules/security/uami.bicep' = { - name: take(replace(deploymentNameStructure, '{rtype}', 'uami-aib'), 64) +// Deploy the imaging resources +module imagingResourcesModule './aib-resources.bicep' = { + name: take(replace(deploymentNameStructure, '{rtype}', 'aib'), 64) + scope: imagingRg params: { location: location tags: tags - uamiName: replace(namingStructure, '{rtype}', 'uami-aib') - } -} - -// Assign the new custom role to the UAMI -module uamiImagingRoleAssignmentModule '../../../module-library/roleAssignments/roleAssignment-rg.bicep' = { - name: take(replace(deploymentNameStructure, '{rtype}', 'uami-img-rbac'), 64) - params: { - principalId: aibUamiModule.outputs.principalId - roleDefinitionId: aibRoleDefinition.id - principalType: 'ServicePrincipal' - } -} - -var aibNetworkAddressPrefix = '192.168.1.0/24' - -module aibNetworkModule '../../../shared-modules/networking/main.bicep' = { - name: take(replace(deploymentNameStructure, '{rtype}', 'network-aib'), 64) - params: { deploymentNameStructure: deploymentNameStructure - location: location - namingStructure: namingStructure - vnetAddressPrefixes: [aibNetworkAddressPrefix] - - subnetDefs: { - ImageBuilderSubnet: { - addressPrefix: cidrSubnet(aibNetworkAddressPrefix, 24, 0) - privateLinkServiceNetworkPolicies: 'Disabled' - // For compliance, all subnets need a NSG - securityRules: [] - } - } - - tags: tags - } -} - -module computeGalleryNameModule '../../../module-library/createValidAzResourceName.bicep' = { - name: take(replace(deploymentNameStructure, '{rtype}', 'galname'), 64) - params: { environment: environment - location: location namingConvention: namingConvention - resourceType: 'gal' sequence: sequence workloadName: workloadName - subWorkloadName: '' - } - dependsOn: [uamiImagingRoleAssignmentModule] -} - -module computeGalleryModule 'br/public:avm/res/compute/gallery:0.3.1' = { - name: take(replace(deploymentNameStructure, '{rtype}', 'gal'), 64) - params: { - name: computeGalleryNameModule.outputs.validName - location: location - - images: [ - { - hyperVGeneration: 'V2' - name: sampleImageName - offer: 'WindowsClient' - osType: 'Windows' - publisher: 'Customer' - sku: 'Windows-11-Enterprise-23H2-Gen2' - - securityType: 'TrustedLaunch' - isAcceleratedNetworkSupported: true - isHibernateSupported: true - osState: 'Generalized' - - // Avoid warnings when using the image from the GUI - maxRecommendedMemory: 4000 - maxRecommendedvCPUs: 128 - minRecommendedMemory: 4 - minRecommendedvCPUs: 2 - - tags: tags - } - ] + imageReference: imageReference + namingStructure: namingStructure - tags: tags - enableTelemetry: enableAvmTelemetry + enableAvmTelemetry: enableAvmTelemetry + sampleImageName: sampleImageName } } -module imageTemplateModule 'br/public:avm/res/virtual-machine-images/image-template:0.1.1' = { - name: take(replace(deploymentNameStructure, '{rtype}', 'img'), 64) - params: { - name: replace(namingStructure, '{rtype}', 'img') - location: location - imageSource: union(imageReference, { type: 'PlatformImage' }) - - // TODO: Load from customizable file - customizationSteps: [ - { - type: 'WindowsUpdate' - filters: [ - 'exclude:$_.Title -like \'*Preview*\'' - 'include:$true' - ] - } - { - type: 'PowerShell' - name: 'Install Microsoft Storage Explorer' - runElevated: true - runAsSystem: true - // TODO: Use main branch - scriptUri: 'https://raw.githubusercontent.com/SvenAelterman/Azure-HubAndSpokeResearchEnclave/main/scripts/PowerShell/Scripts/AIB/Windows/Install-StorageExplorer.ps1' - sha256Checksum: 'a8122168d9700c8e3b2fe03804e181a88fdc4833bbeee19bd42e58e3d85903c5' - } - { - type: 'PowerShell' - name: 'Install azcopy' - runElevated: true - runAsSystem: true - // TODO: Use main branch - scriptUri: 'https://raw.githubusercontent.com/SvenAelterman/Azure-HubAndSpokeResearchEnclave/main/scripts/PowerShell/Scripts/AIB/Windows/Install-AzCopy.ps1' - sha256Checksum: '45453a42a0d8d75f4aecb0e83566078373b3320489431b158f8ea4ae08379e59' - } - ] - - distributions: [ - { - type: 'SharedImage' - sharedImageGalleryImageDefinitionResourceId: '${computeGalleryModule.outputs.resourceId}/images/${sampleImageName}' - excludeFromLatest: false - } - ] - - managedIdentities: { - userAssignedResourceIds: [ - aibUamiModule.outputs.id - ] - } - - subnetResourceId: aibNetworkModule.outputs.createdSubnets.ImageBuilderSubnet.id - - enableTelemetry: enableAvmTelemetry - tags: tags - } -} +output imageDefinitionId string = imagingResourcesModule.outputs.imageDefinitionId diff --git a/research-hub/main.bicep b/research-hub/main.bicep index 2b76c6e..6511d0b 100644 --- a/research-hub/main.bicep +++ b/research-hub/main.bicep @@ -134,6 +134,13 @@ param isAirlockReviewCentralized bool = false @description('The date and time seed for the expiration of the encryption keys.') param encryptionKeyExpirySeed string = utcNow() +/* + * Image Builder + */ + +@description('The subscription ID where the image build will take place. This might need to be different from the hub subscription because image building might not meet compliance requirements.') +param imageBuildSubscriptionId string = subscription().subscriptionId + // TODO: If no custom DNS IPs are specified, create a private DNS zone for the virtual network for VM auto-registration /* @@ -461,13 +468,6 @@ module avdJumpBoxSessionHostModule '../shared-modules/virtualDesktop/sessionHost } } -resource imagingRg 'Microsoft.Resources/resourceGroups@2023-07-01' = { - #disable-next-line BCP334 - name: take(replace(rgNamingStructure, '{subWorkloadName}', 'imaging'), 64) - location: location - tags: actualTags -} - // Default image that will be used to create an Image Template var sampleImageTemplateImageReference = { publisher: 'microsoftwindowsdesktop' @@ -477,7 +477,7 @@ var sampleImageTemplateImageReference = { } module imagingModule 'hub-modules/imaging/main.bicep' = { - scope: imagingRg + scope: subscription(imageBuildSubscriptionId) name: take(replace(deploymentNameStructure, '{rtype}', 'imaging'), 64) params: { location: location @@ -490,6 +490,8 @@ module imagingModule 'hub-modules/imaging/main.bicep' = { enableAvmTelemetry: enableAvmTelemetry imageReference: sampleImageTemplateImageReference namingStructure: replace(resourceNamingStructure, '{subWorkloadName}', 'imaging') + + resourceGroupName: take(replace(rgNamingStructure, '{subWorkloadName}', 'imaging'), 64) } } @@ -545,6 +547,8 @@ output managementVmId string = managementVmModule.outputs.vmId output managementVmUamiPrincipalId string = managementVmModule.outputs.uamiPrincipalId output managementVmUamiClientId string = managementVmModule.outputs.uamiClientId +output imageDefinitionId string = imagingModule.outputs.imageDefinitionId + // TODO: Output the resource ID of the remote application group for the remote desktop application // To be used in the spoke for setting permissions //output remoteDesktopAppGroupResourceId string = virtualDesktopModule.outputs.remoteDesktopAppGroupResourceId diff --git a/scripts/PowerShell/Modules/AzSubscriptionManagement.psm1 b/scripts/PowerShell/Modules/AzSubscriptionManagement.psm1 index 871fbae..062cf88 100644 --- a/scripts/PowerShell/Modules/AzSubscriptionManagement.psm1 +++ b/scripts/PowerShell/Modules/AzSubscriptionManagement.psm1 @@ -161,6 +161,4 @@ Function Register-AzResourceProviderWrapper { Export-ModuleMember -Function Set-AzContextWrapper Export-ModuleMember -Function Register-AzProviderFeatureWrapper - -# TODO: Develop module to register resource providers Export-ModuleMember -Function Register-AzResourceProviderWrapper \ No newline at end of file