When creating a virtual machine in Azure, Microsoft provide a set of images for you to use that your virtual machines get built from. These include simple images such as Server 2012 r2 with nothing installed up to images with different operating systems and various pieces of software already installed, such as Ubuntu, CentOS and many third party applications. There are other reasons why you may want to use a custom image, for example licensing multiple copies of software, it can potentially cost more to do this if you are using images supplied by Microsoft.
For this particular project however, we were in need of an image with such a specific range of needs that none of these supplied images were suitable. We decided we would create a virtual machine in Azure, install all the required software, place all required files where they need to be, sysprep the machine and utilise the image wherever it is needed. It is also possible to create this base image using a Hyper-V VM on your local machine, then upload that for use in Azure.
So the process that I’ve briefly described is relatively simple, but there are a couple gotchas on the way that you should be aware of.
First of all, create a VM in azure. This is easy. Use the Azure portal or a template such as this one. Once it is deployed, RDP to it, and start installing! If you have a whole list of software to install, consider checking out Chocolatey.org. Once you have installed chocolatey, it simplifies installing software by only requiring one cmd line, such as this one that installs Visual Studio 2015 enterprise:
choco install visualstudio2015enterprise
Chocolatey then downloads any files it requires and gets on with installing. This is great for use in automated installations.
Now that you have completed creating your image, you can sysprep the VM. Open the Command Prompt window as an administrator. Change the directory to %windir%\system32\sysprep, and then run sysprep.exe making sure you have the settings the same as shown.
Once the VM has shut down (check in the azure portal. It will say stopped but still incurring compute charges) open up a Powershell session. Then run these commands to set the session to the correct azure subscription:
Login-AzureRmAccountSelect-AzureRmSubscription -SubscriptionId <subscriptionID>
We need to set the status of the VM to deallocated.
Stop-AzureRmVM -ResourceGroupName <resourceGroup> -Name <vmName>
Now sysprepping did generalize the VM, but we need to set the state that Azure sees to generalized, as it is a different kind. Don’t ask me the specifics.
Set-AzureRmVm -ResourceGroupName <resourceGroup> -Name <vmName> -Generalized
Then finally we run this cmdlet to save the image to a page blob. Azure VMs can only use VHDs that are page blobs. Block blobs will not work.
Save-AzureRmVMImage -ResourceGroupName <resourceGroupName> -Name <vmName>-DestinationContainerName <destinationContainerName> -VHDNamePrefix <templateNamePrefix>
This will leave your ready to use image at this location:
https://.blob.core.windows.net/system/Microsoft.Compute/Images//-osDisk.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.vhd
We will use this URL in the next stage of the process.
As this image is for use in an Azure VM, we will be using an ARM template deployment to create our resources in Azure. When you create an Azure Resource Group project in Visual Studio, it automatically provides a script called Deploy-AzureResourceGroup.ps1. This is the script that is ran when you right click deploy on your project. I have made a few modifications to this script that enable the use of a custom image.
For a VM to be able to use the custom image, the image must be present in the storage account that will be used for the OS disk of the VM. And as we are using a template to create all of our resources, we can’t create the storage accounts at that time. Below is the entire section of the script that accomplishes this.
# Create storage accounts for vhd$vhdStorage = Get-AzureRmStorageAccount -ResourceGroupName $ResourceGroupName -Name $DestStorageAccountName -ErrorAction Continue if ($vhdStorage -eq $null) {$vhdStorage = New-AzureRmStorageAccount -ResourceGroupName $ResourceGroupName-AccountName $DestStorageAccountName0 -Location $ResourceGroupLocation -Type "Standard_LRS" }
$storageKey = Get-AzureRmStorageAccountKey -ResourceGroupName $ResourceGroupName -Name $DestStorageAccountName
#Server side storage copy image to storage accounts$SourceStorageAccount = "<storageAccountName>" $SourceStorageKey = "<storageAccountKey>”$Blob = "Microsoft.Compute/Images//-osDisk.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.vhd"$DestStorageKey = $storageKey.value[0]$StorageContainer = 'system'$SourceStorageContext = New-AzureStorageContext –StorageAccountName$SourceStorageAccount -StorageAccountKey $SourceStorageKey $DestStorageContext = New-AzureStorageContext –StorageAccountName $DestStorageAccountName -StorageAccountKey $DestStorageKey
$destinationVHD = Get-AzureStorageBlob -Blob $Blob -Container $StorageContainer -Context $DestStorageContext -ErrorAction SilentlyContinue
if (!$destinationVHD) { New-AzureStorageContainer -Name $StorageContainer -Context $DestStorageContext
$BlobCopy = Start-CopyAzureStorageBlob -Context $SourceStorageContext -SrcContainer $StorageContainer -SrcBlob $Blob -DestContext $DestStorageContext -DestContainer $StorageContainer -DestBlob $Blob -Force
$blobURI = "https://" + $DestStorageAccountName + ".blob.core.windows.net/system/" + $Blob
So now we have the URI of the image that we can use to create VMs in the storage account in the resource group. We can then supply this URI to the template as an inline parameter.
New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) -ResourceGroupName $ResourceGroupName -TemplateFile $TemplateFile -TemplateParameterFile $TemplateParametersFile -vhdURI $blobURI @OptionalParameters -timestamp $timestamp –Verbose
We then use the URI in the storageProfile section of the VM in the ARM template.
"storageProfile": { "osDisk": { "name": "vmosdisk", "caching": "ReadOnly", "createOption": "FromImage", "image": { "uri": "[parameters('vhdURI')]" }, "osType": "Windows" } }
There is an alternative solution to creating a custom image that you may wish to use. This method has an advantage if your image will be changing throughout your project. It can be time-consuming to maintain an image by loading it up on a VM, modifying the state, then sysprepping. Instead all software that you require to be installed could be done using a custom script extension. Simply start off with the base OS image, then you can create a Powershell script that uses chocolatey to install any software you wish to use. So instead of maintaining the image, you are maintaining the scripts that create that image. The trade-off of this process, apart from the obvious initial work of creating and testing your script, is that the deployment will take longer. How much longer depends on the complexity of your script.
And of course, there is always the option of using a combination of these techniques. If you install a large piece of software, such as Visual Studio, onto the base image, then you can use custom scripts to install any extra things you require. This seems an efficient way of doing things. You can even utilise Azure Automation DSC (Desired State Configuration) to do further config of your machine, as well as maintain and modify that state down the line, but that’s a whole other kettle of fish.
I hope you have learnt something from reading this. Look out for another post from me soon, showcasing the full template that this image is being used for.