AzureBakery/AzureBakery.psm1
using namespace Microsoft.Azure.Commands.Common.Authentication.Abstractions Import-Module "AzureRm" <# .SYNOPSIS Bakes Windows Features and Updates into the latest version of Windows Server .DESCRIPTION Creates a VHD in the specified storage container containing the specified Windows Features and all Windows Updates. .PARAMETER StorageContext Azure Storage context used to interact with the Azure Storage PowerShell API. .PARAMETER WindowsFeature Array of Windows Features and Roles to be installed on the generalized image. Feature names must be same as from Get-WindowsFeature. .PARAMETER StorageContainer Blob storage container name where the baked VHD will be placed .PARAMETER TempResourceName Globally unique identifier used to name temporary resources .PARAMETER ImageName Blob name of the baked VHD Default value: "BakedWindows.$(Get-Date -Format "yyMMddHHmm").vhd" .PARAMETER Location Region where temporary resources are created. .EXAMPLE # Get the Azure Storage context $ctx = (Get-AzureRmStorageAccount -Name "contosostorage").Context # Creates a fully updated Windows VHD with the "Web-Server" and "Web-Asp-Net" features installed. # The VHD is located in contosostorage.blob.core.windows.net/images New-BakedImage -StorageContext $ctx -WindowsFeature "Web-Server", "Web-Asp-Net" # Creates a fully updated Windows VHD with the "Web-Server" and "Web-Asp-Net" features installed. New-BakedImage -StorageContext $ctx -WindowsFeature "Web-Server", "Web-Asp-Net" - .NOTES Log into Azure before running this cmdlet #> function New-BakedImage { Param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [IStorageContext]$StorageContext, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]]$WindowsFeature, [ValidateNotNullOrEmpty()] [string]$StorageContainer = "images", [ValidateNotNullOrEmpty()] [string]$TempResourceName = ("temp$(New-Guid)" -replace "[^\w\d]").Substring(0, 24), [ValidateNotNullOrEmpty()] [string]$ImageName = "BakedWindows.$(Get-Date -Format "yyMMddHHmm").vhd", [ValidateNotNullOrEmpty()] [string]$Location = "East US" ) $ErrorActionPreference = "Stop" $InformationPreference = "Continue" $VerbosePreference = "Continue" <## # Fail-fast error checking #> if (-not (Get-AzureRmContext).Account) { throw "Run Login-AzureRmAccount to continue" } Write-Information "Using name '$TempResourceName'" <## # Package DSC #> Write-Information "Packaging DSC" Write-Verbose "Zipping '$PSScriptRoot\dsc.ps1' to '$env:TEMP\dsc.zip'" Publish-AzureRmVMDscConfiguration ` -ConfigurationPath "$PSScriptRoot\dsc.ps1" ` -OutputArchivePath "$env:TEMP\dsc.zip" ` -Force ` | Out-Null <## # Upload artifacts #> Write-Information "Uploading artifacts" Write-Verbose "Creating resource group '$TempResourceName'" New-AzureRmResourceGroup -Name $TempResourceName -Location $Location | Out-Null Write-Verbose "Creating storage account '$TempResourceName'" $storageAccount = New-AzureRmStorageAccount ` -ResourceGroupName $TempResourceName ` -Name $TempResourceName ` -SkuName Standard_LRS ` -Location $Location Write-Verbose "Creating storage container 'artifacts'" New-AzureStorageContainer ` -Name "artifacts" ` -Context $storageAccount.Context ` | Out-Null Write-Verbose "Uploading '$env:TEMP\dsc.zip' to 'artifacts\dsc.zip'" Set-AzureStorageBlobContent ` -File "$env:TEMP\dsc.zip" ` -Container "artifacts" ` -Blob "dsc.zip" ` -Context $storageAccount.Context ` | Out-Null Write-Verbose "Uploading '$PSScriptRoot\cse.ps1' to 'artifacts\cse.ps1'" Set-AzureStorageBlobContent ` -File "$PSScriptRoot\cse.ps1" ` -Container "artifacts" ` -Blob "cse.ps1" ` -Context $storageAccount.Context ` | Out-Null Write-Verbose "Generating 2hr SAS token for 'artifacts'" $sasToken = New-AzureStorageContainerSASToken ` -Name "artifacts" ` -Permission "r" ` -StartTime (Get-Date) ` -ExpiryTime (Get-Date).AddHours(2) ` -Context $storageAccount.Context <## # Deploy #> Write-Information "Deploying" # create the infra and run the DSC Write-Verbose "Deploying resource group" New-AzureRmResourceGroupDeployment ` -Name ((Get-Date -Format "s") -replace "[^\d]") ` -ResourceGroupName $TempResourceName ` -TemplateFile "$PSScriptRoot\template.json" ` -DscUrl "https://$TempResourceName.blob.core.windows.net/artifacts/dsc.zip$sasToken" ` -WindowsFeature $WindowsFeature ` | Out-Null # remove the DSC and generalize the VM Write-Verbose "Get a reference to the VM" $Vm = Get-AzureRmVM -ResourceGroupName $TempResourceName $VmName = $Vm.Name $VmDiskName = $Vm.StorageProfile.OsDisk.Name Write-Verbose "Remove the DSC extension" Remove-AzureRmVMDscExtension -ResourceGroupName $TempResourceName -VMName $VmName | Out-Null Write-Verbose "Saving the Azure context" Save-AzureRmContext -Path "$env:TEMP\.azurebakery.context.json" -Force # sysprep the VM in a job so we can kill it before it times out Write-Verbose "Add the CSE extension to sysprep the VM" $job = Start-Job { Import-AzureRmContext -Path "$env:TEMP\.azurebakery.context.json" Set-AzureRmVMCustomScriptExtension ` -ResourceGroupName $using:TempResourceName ` -VMName $using:VmName ` -Location $using:Location ` -FileUri "https://$using:TempResourceName.blob.core.windows.net/artifacts/cse.ps1$using:sasToken" ` -Run "cse.ps1" ` -Name "SysprepVm" ` -ErrorAction SilentlyContinue } Start-Sleep -Seconds (10 * 60) $job | Stop-Job | Remove-Job -Force # stop and generalize the VM Write-Verbose "Stop the VM" Stop-AzureRmVM -ResourceGroupName $TempResourceName -Name $VmName -Force | Out-Null Write-Verbose "Generalize the VM" Set-AzureRmVM -ResourceGroupName $TempResourceName -Name $VmName -Generalized | Out-Null # copy the internal disk blob to a normal blob storage container # adapted from https://blogs.msdn.microsoft.com/igorpag/2017/03/14/azure-managed-disks-deep-dive-lessons-learned-and-benefits/ Write-Verbose "Lock the OS disk for copy" $diskUrl = Grant-AzureRmDiskAccess ` -ResourceGroupName $TempResourceName ` -DiskName $VmDiskName ` -Access Read ` -DurationInSecond 3600 ` | % {$_.AccessSAS} Write-Verbose "Copy the disk to blob storage" Start-AzureStorageBlobCopy ` -AbsoluteUri $diskUrl ` -DestBlob $ImageName ` -DestContainer $StorageContainer ` -DestContext $StorageContext ` -Force ` | Out-Null Get-AzureStorageBlobCopyState ` -Container $StorageContainer ` -Blob $ImageName ` -Context $StorageContext ` -WaitForComplete ` | Out-Null Write-Verbose "Unlock the OS disk for deletion" Revoke-AzureRmDiskAccess ` -ResourceGroupName $TempResourceName ` -DiskName $VmDiskName ` | Out-Null # clean up Write-Verbose "Delete '$TempResourceName'" # Remove-AzureRmVM -ResourceGroupName $TempResourceName -Name $TempResourceName -Force | Out-Null Remove-AzureRmResourceGroup -Name $TempResourceName -Force | Out-Null return "$($StorageContext.BlobEndPoint)$StorageContainer/$ImageName" } |