Managed Images in Azure (Create & Deploy)

Microsoft has over a thousand Virtual Machine images available in the Microsoft Azure Marketplace. If your organization has their own on-premises “Gold Image” that’s been tailored, hardened, and adapted to meet specific organizational requirements (compliance, business, security, etc.), you can bring those images into your Azure subscription for reuse, automation, and/or manageability.

I recently had the opportunity to take a client’s virtualized Windows Server 2008 R2 “Gold Image” in .OVA format (VMware ), extract the contents using 7-Zip, run the Microsoft Virtual Machine Converter to create a VHD, prepare and upload the VHD, and create a Managed Image that was then deployed using PowerShell and an Azure Resource Manager Template.

It’s actually quite simple! Here’s how…

Benefits

Not only is the client now able to completely automate VM provisioning using this “Gold Image” but a VM created from a Managed Image uses managed disks for the OS and data disks.  This eliminates the hassle of dealing with storage accounts, while providing security using Azure RBAC and avoiding disk storage single points of failure for VMs in an Availability Set.  It also allows for the application of Tags.

Note: Azure does not support Generation 2 virtual machines.

Prepare and Generalize Your Image

I won’t walk through all the steps in preparing and generalizing your image since there are many flavors of Linux with specific requirements, but I will provide the references to get this accomplished.

Prepare a Windows VM

  1. Convert Disk to VHD
  2. Configure Windows for Azure
  3. Set services startup to Windows default values
  4. Update Remote Desktop registry settings
  5. Configure Windows Firewall rules
  6. Verify VM is healthy, secure, and accessible with RDP
  7. Install Windows Updates
  8. Run Sysprep
  9. Complete recommended configurations

Prepare a Linux VM

Please visit Creating and Uploading a Virtual Hard Disk that Contains the Linux Operating System for steps that address the various flavors of Linux.

Uploading an Image (VHD) to Azure Resource Manager

Once your VM has been prepared, it can then be uploaded to Azure for further configuration and use. Once the VHD is uploaded, unmanaged VMs can be immediately created using PowerShell or through Azure Resource Manager Template Deployments.

This post will not cover those specific scenarios but instead skip to creating a Managed Image directly from the uploaded VHD and then using an ARM Template to deploy the Managed Image.

Execute the following script in an elevated PowerShell session to upload your VHD file to a new Resource Group and Azure Blob Storage Account Container.

PowerShell

#Import Modules

Import-Module Azure

Import-Module AzureRm

 

#Login to Azure

Login-AzureRmAccount

$subscriptionName = Get-AzureRmSubscription | Select SubscriptionName | Out-GridView -PassThru

 

#Select Subscription

Select-AzureRmSubscription -SubscriptionName $subscriptionName.SubscriptionName

 

#Select Location

$location = Get-AzureRmLocation | Select Location | Out-GridView -PassThru

 

#Create Resource Group

$resourceGroup = New-AzureRmResourceGroup -Name "changeMe" -Location $location.Location

 

#Create Storage Account

$storageAccount = New-AzureRmStorageAccount -ResourceGroupName $resourceGroup.ResourceGroupName -Name "changeMeLowerCaseNoSpaces" -Location $location.Location -SkuName Standard_LRS

 

#Create Blob Storage Container

$container = New-AzureStorageContainer -Name "vhd-images" -Permission Off -Context $storageAccount.Context

 

#Upload VHD

$azureVHDName = "changeMe.vhd"

$azureVHDUrl = "$($container.CloudBlobContainer.Uri.AbsoluteUri)/$azureVHDName"

Add-AzureRmVhd -ResourceGroupName $resourceGroup.ResourceGroupName -Destination $azureVHDUrl -LocalFilePath "c:\change\local\path\to\localVHDName.vhd"

Convert Unmanaged VHD to Managed Image

Assumptions

  • The Resource Group created previously will be the home for the Managed Image

Using the same PowerShell session and variables from our previous PowerShell session, we can execute the following:

PowerShell

$vmOSType = 'Windows' ## OR 'Linux'

$imageName = 'changeMe' ## Give it a name

 

$imageConfig = New-AzureRmImageConfig -Location $location.Location

$imageConfig = Set-AzureRmImageOsDisk -Image $imageConfig -OsType $vmOSType -OsState Generalized -BlobUri $azureVHDUrl

$image = New-AzureRmImage -ImageName $imageName -ResourceGroupName $resourceGroup.ResourceGroupName -Image $imageConfig

Too easy!

Deploy Managed Image Using an ARM Template

Now let’s put it all together to create a Managed VM with a single Managed Data Disk. If you desire to have Data Disks created in a dynamic fashion, please view my Azure/azure-quickstart-templates branch by going here. We will use an ARM template and PowerShell.

Assumptions

  • The Resource Group created previously will be the home for all resources in this deployment
  • A VNet and Subnet already exists
  • An Availability Set will not be provisioned

Copy and Save the below JSON to a known location with a file extension .json:

Resources

  • Network Security Group
  • Public IP address
  • Network Interface
  • Managed disk
  • Virtual Machine
{

"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",

"contentVersion": "1.0.0.0",

"parameters": {

"location": { "type": "string" },

"vmName": { "type": "string" },

"vnetName": { "type": "string" },

"subnetName": { "type": "string" },

"localAdminUserName": { "type": "string" },

"localAdminPassword": { "type": "securestring" },

"imageName": { "type": "string" },

"vmOSType": {

"type": "string",

"allowedValues": [

"Windows",

"Linux"

]

}

},

"variables": {

"vnetResourceID": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]",

"subnetResourceID": "[concat(variables('vnetResourceID'), '/subnets/', parameters('subnetName'))]",

"nsgPort": {

"Windows": 3389,

"Linux": 22

}

},

"resources": [

{

"type": "Microsoft.Network/networkInterfaces",

"apiVersion": "2016-03-30",

"location": "[parameters('location')]",

"dependsOn": [

"[concat(parameters('vmName'),'-nsg')]",

"[concat(parameters('vmName'),'-pip')]"

],

"tags": {},

"name": "[concat(parameters('vmName'),'-nic')]",

"properties": {

"ipConfigurations": [

{

"name": "ipconfig1",

"properties": {

"privateIPAllocationMethod": "Dynamic",

"publicIPAddress": {

"id": "[resourceId('Microsoft.Network/publicIPAddresses',concat(parameters('vmName'),'-pip'))]"

},

"subnet": {

"id": "[variables('subnetResourceID')]"

}

}

}

],

"networkSecurityGroup": {

"id": "[resourceId('Microsoft.Network/networkSecurityGroups',concat(parameters('vmName'),'-nsg'))]"

}

}

},

{

"type": "Microsoft.Network/networkSecurityGroups",

"apiVersion": "2016-03-30",

"location": "[parameters('location')]",

"dependsOn": [],

"tags": {},

"name": "[concat(parameters('vmName'),'-nsg')]",

"properties": {

"securityRules": [

{

"name": "[concat(parameters('vmName'),'-rule-remote-access')]",

"properties": {

"protocol": "TCP",

"sourcePortRange": "*",

"destinationPortRange": "[variables('nsgPort')[parameters('vmOSType')]]",

"sourceAddressPrefix": "*",

"destinationAddressPrefix": "*",

"access": "Allow",

"priority": 110,

"direction": "Inbound"

}

}

]

}

},

{

"type": "Microsoft.Network/publicIPAddresses",

"apiVersion": "2016-03-30",

"location": "[parameters('location')]",

"dependsOn": [],

"tags": {},

"name": "[concat(parameters('vmName'),'-pip')]",

"properties": {

"publicIPAllocationMethod": "Dynamic"

}

},

{

"type": "Microsoft.Compute/disks",

"apiVersion": "2017-03-30",

"location": "[parameters('location')]",

"tags": {},

"name": "[concat(parameters('vmName'),'-datadisk-1')]",

"properties": {

"diskSizeGB": 128,

"creationData": {

"createOption": "Empty"

}

}

},

{

"type": "Microsoft.Compute/virtualMachines",

"apiVersion": "2015-06-15",

"location": "[parameters('location')]",

"name": "[parameters('vmName')]",

"dependsOn": [

"[resourceId('Microsoft.Network/networkInterfaces', concat(parameters('vmName'),'-nic'))]",

"[resourceId('Microsoft.Compute/disks',concat(parameters('vmName'),'-datadisk-1'))]"

],

"tags": {},

"properties": {

"hardwareProfile": {

"vmSize": "Standard_A1_v2"

},

"osProfile": {

"computerName": "[parameters('vmName')]",

"adminUsername": "[parameters('localAdminUserName')]",

"adminPassword": "[parameters('localAdminPassword')]"

},

"storageProfile": {

"imageReference": {

"id": "[resourceId('Microsoft.Compute/images',parameters('imageName'))]"

},

"osDisk": {

"name": "[concat(parameters('vmName'),'-OSDisk')]",

"caching": "ReadWrite",

"createOption": "FromImage",

"diskSizeGB": 128,

"managedDisk": {

"storageAccountType": "Standard_LRS"

}

},

"dataDisks": [

{

"managedDisk": {

"id": "[resourceId('Microsoft.Compute/disks',concat(parameters('vmName'),'-datadisk-1'))]"

},

"lun": 0,

"caching": "None",

"createOption": "Attach"

}

]

},

"networkProfile": {

"networkInterfaces": [

{

"id": "[resourceId('Microsoft.Network/networkInterfaces', concat(parameters('vmName'),'-nic'))]"

}

]

}

}

}

],

"outputs": {}

}

Using the same PowerShell session and variables from our previous PowerShell session, we can deploy the ARM Template using the following PowerShell:

PowerShell

$templateFilePath = 'c:\change\to\path\of\above\saved\file.json'

 

$parameterObject = @{

'location' = $location.Location

'vmName' = 'changeMe'

'vnetName' = 'changeMe'

'subnetName' = 'changeMe'

'localAdminUserName' = 'changeMe'

'localAdminPassword' = 'changeMeToComplex'

'imageName' = $imageName

'vmOSType' = 'Windows' ## OR 'Linux'

}

 

New-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroup.ResourceGroupName `

-TemplateFile $templateFilePath `

-TemplateParameterObject $parameterObject `

-Name 'MyUnmanagedDeployment' `

-Mode Incremental `

-Verbose -ErrorAction Stop;

 

Note

If errors are received when attaching the Managed Data Disk, you can comment out the dataDisks property as detailed below and try adding the disk manually through the portal. The reason for error is usually due to not properly preparing the VHD prior to upload.

/*

"dataDisks": [

{

"managedDisk": {

"id": "[resourceId('Microsoft.Compute/disks',concat(parameters('vmName'),'-datadisk-1'))]"

},

"lun": 0,

"caching": "None",

"createOption": "Attach"

}

]

*/

 

About Justin Baca

Justin is an Infrastructure Consultant with over 16 years of experience in IT (DoD, Gov, and Commercial). He has planned, designed, and implemented small, medium, and large scale networks. Just is also part of the DevOps team where he focuses on automation, reuse, and CI/CD.