Speaking at Azure Global Bootcamp in Cluj
[/content/images/wordpress/2017/04/2017bootcamp.png] For the 5th year in a row, ITCamp Community is organising Global Azure Boot Camp (https
If you worked with Azure for a long time, you know that when you wanted to upload your own custom VM image to Azure, it was an easy thing. You prepared the VM, you sent it to Azure using PowerShell and after that you tagged it as an OS disk and that was it. Well that was the old way using the Azure Service Manager which I must say it was quite an easy procedure. With Azure Resource Manager, things changed quite a bit. You still have the possibility of uploading the VHDs to Azure but the deployment requires a little more work. You have to write code for that deployment to happen, be it in PowerShell or JSON. In this blog post I’m going to give you two ARM templates which you can use to deploy your freshly uploaded VHDs.
While I like PowerShell very much and I use non-stop for everything, Azure Resource Manager makes writing PowerShell for deploying and managing stuff a bit harder. Not impossible but harder, so here I am, writing JSON code for deploying and managing resources in Azure and boy let me tell you that it’s going to take a while till you get the hang of them, and the earlier you start understanding how ARM templates, the better.
At this point in time, there’s no easy way in writing these ARM Templates. We have the possibility of using Visual Studio paired with the Azure SDK, which can give you a kick in the right direction but it will not help a lot because it’s kinda outdated. So the best way to create these ARM templates is by using other available templates and adapt them to your needs. You can get all the examples that you want from the Microsoft Azure Github repo – Azure ARM Templates –
In the Azure Github repository there are two examples that you can use to create a VM from a specialized image, but those templates just shows you how it works and they can’t be used for production deployments without doing some work.
The templates attached to this blog post will allow you to create a Windows or Linux VM using a VHD that was uploaded into an Azure Storage Account. Please note that at this time, you cannot have a “golden image” in a separate Storage Account due to an Azure limitation. There is a feedback post about this issue which unfortunately is under review for a while, and I hope that this will be solved soon™.
In order to use these templates, you need to have the .vhd of a deprovisioned Linux or sysprepped Windows virtual machine in a Storage Account and you need the URI of the VHD that will be used for the deployment.
You can get the URI of your uploaded VHD(s) by using the following Azure PowerShell script:
#requires -Version 1 -Modules Azure.Storage, AzureRM.Storage $ResourceGroupName = 'RGNName' $StorageAccountName = 'SAName' $ContainerName = 'ContainerName' $StorageContainer = Get-AzureRmStorageAccount -ResourceGroupName $ResourceGroupName -Name $StorageAccountName | ` Get-AzureStorageContainer -Container $ContainerName -ErrorAction SilentlyContinue (Get-AzureStorageBlob -Context $StorageContainer.Context -Container $ContainerName).ICloudBlob.StorageUri.PrimaryUri.AbsoluteUri
Now that you have everything set, all you need to do is to start deploying ?
The templates attached in this blog post cover two scenarios and are adapted for when that Azure limitation is lifted so you can have your golden image in a separate storage account.
{ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "adminUserName": { "type": "string", "metadata": { "description": "UserName for the Virtual Machine" } }, "adminPassword": { "type": "securestring", "metadata": { "description": "Password for the Virtual Machine" } }, "osDiskVhdUri": { "type": "string", "metadata": { "description": "Uri of the existing VHD in ARM standard or premium storage" } }, "storageAccountName": { "type": "string", "metadata": { "description": "This is the name of the your storage account" } }, "vmName": { "type": "string", "metadata": { "description": "The name of the virtual machine" } }, "dnsNameForPublicIP": { "type": "string", "metadata": { "description": "DNS Label for the Public IP. Must be lowercase. It should match with the following regular expression: ^[a-z][a-z0-9-]{1,61}[a-z0-9]$ or it will raise an error." } }, "virtualNetworkName": { "type": "string", "metadata": { "description": "Name for the Virtual Network." } }, "osType": { "type": "string", "allowedValues": [ "Windows", "Linux" ], "metadata": { "description": "This is the OS that your VM will be running" } }, "vmSize": { "type": "string", "defaultValue": "Standard_D1_v2", "metadata": { "description": "This is the size of your VM" } } }, "variables": { "computeApiVersion": "2016-03-30", "networkApiVersion": "2016-03-30", "storageApiVersion": "2015-06-15", "insightsApiVersion": "2015-04-01", "location": "[resourceGroup().location]", "publicIPAddressName": "[parameters('vmName')]", "virtualNetworkName": "[parameters('vmName')]", "nicName": "[parameters('vmName')]", "addressPrefix": "10.0.0.0/16", "subnet1Name": "default", "subnet1Prefix": "10.0.0.0/24", "publicIPAddressType": "Dynamic", "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]", "subnet1Ref": "[concat(variables('vnetID'),'/subnets/',variables('subnet1Name'))]", "osDiskVhdName": "[concat('http://',parameters('storageAccountName'),'.blob.core.windows.net/vhds/',parameters('vmName'),'osDisk.vhd')]", "nsgName": "[parameters('vmName')]", "Windows": { "name": "default-allow-rdp-connections", "properties": { "description": "Allow RDP Connections", "protocol": "Tcp", "sourcePortRange": "*", "destinationPortRange": "3389", "sourceAddressPrefix": "*", "destinationAddressPrefix": "*", "access": "Allow", "priority": 1000, "direction": "Inbound" } }, "Linux": { "name": "default-allow-ssh-connections", "properties": { "description": "Allow SSH Connections", "protocol": "Tcp", "sourcePortRange": "*", "destinationPortRange": "22", "sourceAddressPrefix": "*", "destinationAddressPrefix": "*", "access": "Allow", "priority": 1000, "direction": "Inbound" } }, "portReference": "[variables(parameters('osType'))]" }, "resources": [ { "apiVersion": "[variables('networkApiVersion')]", "type": "Microsoft.Network/publicIPAddresses", "name": "[variables('publicIPAddressName')]", "location": "[variables('location')]", "properties": { "publicIPAllocationMethod": "[variables('publicIPAddressType')]", "dnsSettings": { "domainNameLabel": "[parameters('dnsNameForPublicIP')]" } } }, { "apiVersion": "[variables('networkApiVersion')]", "type": "Microsoft.Network/networkSecurityGroups", "name": "[variables('nsgName')]", "location": "[variables('location')]", "properties": { "securityRules": [ "[variables('portReference')]" ] } }, { "apiVersion": "[variables('networkApiVersion')]", "type": "Microsoft.Network/virtualNetworks", "name": "[variables('virtualNetworkName')]", "location": "[variables('location')]", "properties": { "addressSpace": { "addressPrefixes": [ "[variables('addressPrefix')]" ] }, "subnets": [ { "name": "[variables('subnet1Name')]", "properties": { "addressPrefix": "[variables('subnet1Prefix')]" } } ] } }, { "apiVersion": "[variables('networkApiVersion')]", "type": "Microsoft.Network/networkInterfaces", "name": "[variables('nicName')]", "location": "[variables('location')]", "dependsOn": [ "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]", "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]", "[concat('Microsoft.Network/networkSecurityGroups/', variables('nsgName'))]" ], "properties": { "networkSecurityGroup": { "id": "[resourceId('Microsoft.Network/networkSecurityGroups',variables('nsgName'))]" }, "ipConfigurations": [ { "name": "ipconfig1", "properties": { "privateIPAllocationMethod": "Dynamic", "publicIPAddress": { "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" }, "subnet": { "id": "[variables('subnet1Ref')]" } } } ] } }, { "apiVersion": "[variables('computeApiVersion')]", "type": "Microsoft.Compute/virtualMachines", "name": "[parameters('vmName')]", "location": "[variables('location')]", "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" ], "properties": { "hardwareProfile": { "vmSize": "[parameters('vmSize')]" }, "osProfile": { "computerName": "[parameters('vmName')]", "adminUsername": "[parameters('adminUsername')]", "adminPassword": "[parameters('adminPassword')]" }, "storageProfile": { "osDisk": { "name": "[concat(parameters('vmName'),'-osDisk')]", "osType": "[parameters('osType')]", "caching": "ReadWrite", "createOption": "FromImage", "image": { "uri": "[parameters('osDiskVhdUri')]" }, "vhd": { "uri": "[variables('osDiskVhdName')]" } } }, "networkProfile": { "networkInterfaces": [ { "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" } ] }, "diagnosticsProfile": { "bootDiagnostics": { "enabled": true, "storageUri": "[concat('http://',parameters('storageAccountName'),'.blob.core.windows.net')]" } } } } ], "outputs": { "hostname": { "type": "string", "value": "[concat(parameters('dnsNameForPublicIP'), '.', variables('location'), '.cloudapp.azure.com')]" } } }
==========================
{ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "adminUserName": { "type": "string", "metadata": { "description": "UserName for the Virtual Machine" } }, "adminPassword": { "type": "securestring", "metadata": { "description": "Password for the Virtual Machine" } }, "vmName": { "type": "string", "metadata": { "description": "Name of the VM" } }, "osType": { "type": "string", "allowedValues": [ "Windows", "Linux" ], "metadata": { "description": "Type of OS on the existing vhd" } }, "osDiskVhdUri": { "type": "string", "metadata": { "description": "Uri of the existing VHD in ARM standard or premium storage" } }, "storageAccountName": { "type": "string", "metadata": { "description": "This is the name of the storage account where the VM will be created" } }, "vmSize": { "type": "string", "defaultValue": "Standard_D1_v2", "metadata": { "description": "Size of the VM" } }, "existingVirtualNetworkName": { "type": "string", "metadata": { "description": "Name of the existing VNET" } }, "subnetName": { "type": "string", "metadata": { "description": "Name of the subnet in the virtual network you want to use" } }, "dnsNameForPublicIP": { "type": "string", "metadata": { "description": "DNS Label for the Public IP. Must be lowercase. It should match with the following regular expression: ^[a-z][a-z0-9-]{1,61}[a-z0-9]$ or it will raise an error." } } }, "variables": { "computeApiVersion": "2016-03-30", "networkApiVersion": "2016-03-30", "storageApiVersion": "2015-06-15", "insightsApiVersion": "2015-04-01", "location": "[resourceGroup().location]", "publicIPAddressType": "Dynamic", "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',parameters('existingVirtualNetworkName'))]", "subnetRef": "[concat(variables('vnetID'),'/subnets/', parameters('subnetName'))]", "nicName": "[parameters('vmName')]", "nsgName": "[parameters('vmName')]", "publicIPAddressName": "[parameters('vmName')]", "osDiskVhdName": "[concat('http://',parameters('storageAccountName'),'.blob.core.windows.net/vhds/',parameters('vmName'),'osDisk.vhd')]", "Windows": { "name": "default-allow-rdp-connections", "properties": { "description": "Allow RDP Connections", "protocol": "Tcp", "sourcePortRange": "*", "destinationPortRange": "3389", "sourceAddressPrefix": "*", "destinationAddressPrefix": "*", "access": "Allow", "priority": 1000, "direction": "Inbound" } }, "Linux": { "name": "default-allow-ssh-connections", "properties": { "description": "Allow SSH Connections", "protocol": "Tcp", "sourcePortRange": "*", "destinationPortRange": "22", "sourceAddressPrefix": "*", "destinationAddressPrefix": "*", "access": "Allow", "priority": 1000, "direction": "Inbound" } }, "portReference": "[variables(parameters('osType'))]" }, "resources": [ { "apiVersion": "[variables('networkApiVersion')]", "type": "Microsoft.Network/publicIPAddresses", "name": "[variables('publicIPAddressName')]", "location": "[variables('location')]", "tags": { "displayName": "PublicIPAddress" }, "properties": { "publicIPAllocationMethod": "[variables('publicIPAddressType')]", "dnsSettings": { "domainNameLabel": "[parameters('dnsNameForPublicIP')]" } } }, { "apiVersion": "[variables('networkApiVersion')]", "type": "Microsoft.Network/networkSecurityGroups", "name": "[variables('nsgName')]", "location": "[variables('location')]", "properties": { "securityRules": [ "[variables('portReference')]" ] } }, { "apiVersion": "[variables('networkApiVersion')]", "type": "Microsoft.Network/networkInterfaces", "name": "[variables('nicName')]", "location": "[variables('location')]", "dependsOn": [ "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]", "[concat('Microsoft.Network/networkSecurityGroups/', variables('nsgName'))]" ], "tags": { "displayName": "NetworkInterface" }, "properties": { "networkSecurityGroup": { "id": "[resourceId('Microsoft.Network/networkSecurityGroups',variables('nsgName'))]" }, "ipConfigurations": [ { "name": "ipconfig1", "properties": { "privateIPAllocationMethod": "Dynamic", "publicIPAddress": { "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" }, "subnet": { "id": "[variables('subnetRef')]" } } } ] } }, { "apiVersion": "[variables('computeApiVersion')]", "type": "Microsoft.Compute/virtualMachines", "name": "[parameters('vmName')]", "location": "[variables('location')]", "tags": { "displayName": "VirtualMachine" }, "dependsOn": [ "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" ], "properties": { "hardwareProfile": { "vmSize": "[parameters('vmSize')]" }, "osProfile": { "computerName": "[concat(parameters('vmName'))]", "adminUsername": "[parameters('adminUsername')]", "adminPassword": "[parameters('adminPassword')]" }, "storageProfile": { "osDisk": { "name": "[concat(parameters('vmName'),'-osDisk')]", "osType": "[parameters('osType')]", "caching": "ReadWrite", "createOption": "FromImage", "image": { "uri": "[parameters('osDiskVhdUri')]" }, "vhd": { "uri": "[variables('osDiskVhdName')]" } } }, "networkProfile": { "networkInterfaces": [ { "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" } ] }, "diagnosticsProfile": { "bootDiagnostics": { "enabled": "true", "storageUri": "[concat('http://',parameters('storageAccountName'),'.blob.core.windows.net')]" } } } } ], "outputs": { "hostname": { "type": "string", "value": "[concat(parameters('dnsNameForPublicIP'), '.', variables('location'), '.cloudapp.azure.com')]" } } }
==========================
The first template deploys a new VM based on that specialized image that you have uploaded, and it creates a Public Dynamic IP, Virtual Network and Network Security Group with RDP / SSH access. The second template covers the scenario where you already have a some virtual machines deployed in a Resource Group and in a Virtual Network, and you want to deploy one or more VMs from that specialized VHD that you uploaded. The difference between the first and second one is that in the second one has mandatory parameters where you need to specify the existing Virtual Network and in which subnet you want the new VM to be deployed in.
Now at this point, all you need to do is to copy the templates and use them either in the portal as a Template Deployment, or save the contents in a .json file and deploy them using PowerShell.
I for one prefer the PowerShell way so here’s the PowerShell script that you can use to deploy the .json templates:
#requires -Version 1 -Modules Azure.Storage, AzureRM.Storage $ResourceGroupName = 'RGNName' $StorageAccountName = 'SAName' $ContainerName = 'ContainerName' $StorageContainer = Get-AzureRmStorageAccount -ResourceGroupName $ResourceGroupName -Name $StorageAccountName | ` Get-AzureStorageContainer -Container $ContainerName -ErrorAction SilentlyContinue $osDiskVhdUri = (Get-AzureStorageBlob -Context $StorageContainer.Context -Container $ContainerName).ICloudBlob.StorageUri.PrimaryUri.AbsoluteUri #Scenario 1: $Parameters = @{ adminUserName = '' adminPassword = '' osDiskVhdUri = $osDiskVhdUri[0].ToString() storageAccountName = '' vmName = '' dnsNameForPublicIP = '' virtualNetworkName = '' osType = '' vmSize = '' } New-AzureRmResourceGroupDeployment -ResourceGroupName $ResourceGroupName -TemplateFile 'In the end, ARM Templates may look scary at first but once you take the time and analyze them, you will notice that they aren't any big deal at all. So my advice to you is; Look at the examples in the Azure GitHub repo and start small.
That's it, have a good one!