Deploy VMs from custom image

Back in the old times, you might have used CDs to deploy a new desktop or server on premise. Or maybe System Center or any other tools. You might have created a "Golden Image" fully prepared and customized, or you used a basic image and tools like System Center, DSC, Puppet or Chef to customize your image after deployment. Well, good news is that most of those options are available in the cloud, too. If you go for the later one, just use a base image provided by Azure and include an extension for DSC or other tools to customize. If you want to use your own image, here is how to do it...

I published already an article touching this for Azure Service Manager deployments (see here (in German, I'm afraid), but meanwhile you should plan with the Azure Resource Manager for deployments and that's what I will show here:

  • we use an existing VM and customize it (just a little bit, feel free to experiment further)
  • we prepare the VM to be used as an image
  • we deploy a new VM based on that image by using a template

We will do this with Windows, but it works for Linux in nearly the same way. Watch the end of this article for the differences!

Ready? Let's go!

Customize Image

As said we will use an existing VM, for this example the VM is named "golden" and is in the resourcegroup "winvm-rg". Best practice is to build a new VM from scratch and customize it. For your first tests you might just change some registry keys, add a folder or install a feature (I always use the telnet client for tests). Iterate your way and use the experience you already have from your on-premise history...

Prepare VM

Good news is that you can use good, old sysprep.exe (located in %windir%\system32\sysprep). Once you are finished with customization, start it, choose (as usual) the "out-of-box-experience", click "generalize", and "shutdown". Or use this commandline after switching to the directory:
[code]
sysprep.exe /generalize /oobe /shutdown

Your customized VM will be prepared and shut down. Remember: If you restart that VM again, you have to go through that process again.

This was the first part, what comes next is to prepare the VM as image. There are two ways to do this. One is to create an "unmanaged image", the other is - you guess it - a "managed image". We will start with the "managed image" and have a short look on the "unmanaged image" later.

First, stop the VM (yes, the VM is in a shutdown state, but is not stopped!):

[code]
Stop-AzureRmVM -ResourceGroupName winvm-rg -Name golden

Second, generalize the OS disk:

[code]
Set-AzureRmVm -ResourceGroupName winvm-rg -Name golden -Generalized

Verify that the VM is in a generalized state:

[code]
Get-AzureRmVm -ResourceGroupName winvm-rg -Name golden -Status

You should see something like this:

[code highlight="4"]
(...)
Code : OSState/generalized
Level : Info
DisplayStatus : VM generalized
(...)

We will go on from here creating a managed image. For information how to create an unmanaged image (and what's the difference) see further down.

For the next steps, we first need some information.

  1. the region where the image will be available, we will use germanycentral
  2. the OSType, we will use Windows
  3. the URI of the VHD of our "golden" VM
  4. a (new?) resourcegroup to store the image reference

Let's look at no. 3. How do we get this information? Well, the URI is part of the storage profile of the VM, and we find it there under OSDisk and Vhd:

[code]
$temp = Get-AzureRmVm -ResourceGroupName winvm-rg -Name golden
$uri = $temp.StorageProfile.OSDisk.Vhd.Uri

Copy the Uri or save it in a variable like we did.

If you want to use a central resource group for all images, create it:

[code]
New-AzureRmResourceGroup -name "goldenimages" -location germanycentral

And now bring it all together to create the image:

[code]
$imgConf = New-AzureRmImageConfig -Location germanycentral
$imgConf = Set-AzureRmImageOsDisk -Image $imgConf -OSType Windows -OSState Generalized -BlobUri $uri
$image=New-AzureRmImage -ImageName "TestImage" -ResourceGroupName goldenimages -Image $imgConf

Needless to say that you should choose a better name then "TestImage". If you look at $image, you see the Id of the new image, or use Get-AzureRmImage for a list of your images. We need this Id for our image reference in the template.

Deploy a new VM

Time to use it in a template. Very simple, just use a normal VM deployment template and change two things:

Define a variable with the Id from above ($image.Id), this will look someting like:
[code highlight="3"]
(...)
"variables": {
"imageID": "/subscriptions/{...}/resourceGroups/goldenimages/providers/Microsoft.Compute/images/TestImage",
(...)

Use this variable in the Microsoft.Compute/virtualMachines resource definition:

[code highlight="4"]
(...)
"storageProfile": {
"imageReference": {
"id": "[variables('ImageId')]"
}
}
(...)

and there we go. If you combine this with a deployment link like explained in this blog article you can build your own internal marketplace. Since I published the template used for this example on GitHub, you can see (but not deploy) all of this for copy&paste. The reason you cannot deploy is that you don't have access to my resources, especially the resourcegroup "goldenimages". To give access to a resourcegroup is left as an exercise to the reader :-).

Unmanaged Images

Unmanaged images are stored in a storage blob. The way to create them is similar to the one used above. Customize and sysprep the VM, stop it and generalize it.

Then create the image:

[code]
Save-AzureRmVmImage -ResourceGroupName winvm-rg -Name golden -DestinationContainerName "winimages" -VHDNamePrefix "prefix" -path c:\temp\filename.json

This will copy the VHD from the VM into a new container inside the same storage account. After the command finished, you find a JSON file at the location you entered (here c:\temp\filename.json) with some information, and under "resources"-"storageProfile"-"osDisk"-"image"-"uri" is the path to that image. The path is contructed like this:

[code]
https://{storageaccount}.blob.core.cloudapi.de/system/Microsoft.Compute/Images/{DestinationContainer}/{prefix}-osDisk...

To use that image in a deployment, make sure that the container has appropriate Read Permissions, maybe copy the image to another container/storageaccount and modify your template as shown:

[code highlight="3,4,5,33,38"]
(...)
"variables": {
"srcImageUri": "https://goldenimages.blob.core.cloudapi.de/windows/master-Win2012R2-v3.vhd",
"srcImageOS": "Windows",
"ImageResourceName": "myImage",
(...)
"resources": [
{
"type": "Microsoft.Compute/images",
"apiVersion": "2016-04-30-preview",
"name": "[variables('ImageResourceName')]",
"location": "[resourceGroup().location]",
"properties": {
"storageProfile": {
"osDisk": {
"osType": "[variables('srcImageOS')]",
"osState": "Generalized",
"blobUri": "[variables('srcImageUri')]",
"storageAccountType": "Standard_LRS"
}
}
}
},
(...)
{
"apiVersion": "2017-03-30",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]",
"[resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))]",
"[resourceId('Microsoft.Compute/images/', variables('ImageResourceName'))]"
],
"properties": {
"storageProfile": {
"imageReference": {
"id": "[resourceId('Microsoft.Compute/images/', variables('ImageResourceName'))]"
}
},
(...)

To explain this: We create an additional resource, the image, and use this resource later for the VM. Notice that I moved the image to a new location in another storage account. You find the full template also on GitHub.

Additions

Linux

As promised, here is in short what to do with Linux. It is nearly identical, just replace the sysprep stuff with waagent.

[code]
sudo waagent -deprovision+user -force

and replace "Windows" with "Linux" when creating the image. See further down unter "Links" for more information.

Preparations

Another thing is where you customize your VM. I prefer Azure, but using a Hyper-V image is also possible of course. You have to do some adoptions, especially watch out for using a fixed disk and a .vhd instead of a .vhdx, but after uploading the VHD into Azure storage the rest of the steps are the same. For preparation see further down under "Links".

Post-deployment tasks

Of course you can add extensions to the VM for DSC etc to customize the VM even further. Sometimes it's a thin line between putting everything into the image or keep some customizations for DSC. Discussions about this is far beyond the scope of this article...

Here are some links to really good AzureDocs articles about image creation, including PowerShell and CLI creation of VMs from managed and unmanaged images. Check them out, they are worth it...

All examples are tested in Azure Germany. Here is where you get a free Azure Germany trial!