Kirk Evans Blog

.NET From a Markup Perspective

Azure Resource Manager Templates with Visual Studio 2015

This post will show you how to create an Azure Resource Manager template using Visual Studio 2015.

Background

In a previous post, I talked about Creating Dev and Test Environments with Windows PowerShell and showed how to create a virtual network with 3 subnets, and how to create 3 environments that each have 2 virtual machines in an availability set to each of the subnets.

image

I got some honest feedback from friends saying that I should stop showing how to use the service management API and should instead start showing examples of how to use Azure Resource Manager.

At the time, I didn’t really know Azure Resource Manager, although I’ve certainly seen it a number of times.  When I looked at the templates, it looked like a bunch of scary JSON, and I didn’t use the new Azure portal very often.  Since then, I’ve put in a bit more time and thought I would show you how you can create your own Azure Resource Manager template to create an environment that looks like the above.

The template for the following post is available on GitHub at https://github.com/kaevans/DevTestProd.

If you are interested in additional ARM templates, there is a gallery of them at https://github.com/Azure/azure-quickstart-templates.

Getting Started

I am using Visual Studio 2015 RC still as Visual Studio 2015 isn’t due to release for 2 more weeks yet.  Create a new Azure Resource Group project.

image

At this point you can choose from a number of templates to get you started.  For instance, there is a template for “Windows Server Virtual Machines with Load Balancer” that has pretty much what we want to create for a single environment.

image

Clicking that will create the following:

image

Two files are created. LoadBalancedVirtualMachine.json is the Azure Resource Manager (ARM) template that describes the resources to be added or updated.  LoadBalancedVirtualMachine.param.dev.json is a parameter file that enables you to provide parameter values for the template.

The JSON Outline pane separates the template into its 3 parts: parameters, variables, and resources.

The parameters section contains the parameters that you will provide for the template.  You can provide the parameters as a JSON file, or the end user can use the new portal to provide values.  You’ll see this when we deploy the template.

image

The variables section contains the variables used internally within the template’s resources.  These variables help you avoid hard-coding values into the resources themselves, and enable you to promote values to become parameters later.  For instance, instead of having a variable “availabilitySetName”, you might decide to make that a user-defined parameter instead.

image

Finally, the resources section defines the resources to be added or updated during the deployment.  This was the part that scared me as I saw some crazy-looking JSON and thought, “oh hell no, that’s worse than editing XML by hand”.

image

Thankfully, using the Visual Studio 2015 tooling and just a little bit of cleverness, we can tailor this template to our liking.

Delete What You Don’t Need

Now that I’ve been working with these tools, I think the best way to get started is to use a template that looks like what you want, and then start deleting the things you don’t need.  For instance, this template includes a load balancer that wasn’t in my original design, so I am going to delete it and the “loadBalancerName” parameter from the JSON Outline.  Visual Studio then highlights two red markers.  I scroll down to that red marker, and see that there is a reference in the template to the parameter.

image

Easy enough, I don’t need a load balancer, so I delete that line of code.

image

Rinse and repeat until the red marks are all gone in the margin.

Edit What You Want

The next step is to make edits.  Right now, the template represents a single environment, but we want 3 environments of 2 VMs each, deployed into 3 different subnets.  This means we will do quite a bit of editing.

Availability Sets

Click on the Availability Set node in the JSON Outline.  Notice that the availability set resource uses a variable “availabilitySetName”.

image

We are going to have 3 availability sets (one for dev, stage, and prod, respectively).  Let’s change that variable to “devAvailabilitySetName” with a value “DevAvSet”.

image

We now have red marks in the margin.

image

Scroll down to the red marks and correct the errors.

DevAvailabilitySet
  1. {
  2.   "apiVersion": "2015-05-01-preview",
  3.   "type": "Microsoft.Compute/availabilitySets",
  4.   "name": "[variables('devAvailabilitySetName')]",
  5.   "location": "[resourceGroup().location]",
  6.   "tags":
  7.   {
  8.     "displayName": "DevAvailabilitySet"
  9.   }
  10. },

We have now defined the availability set for the dev environment, we will come back to this and create one for stage and prod in a little bit.

Virtual Networks

We currently have a virtual network with a single subnet.  Let’s add a few subnets.  Instead of including the subnet name, location, address space, subnet names, or subnet prefixes in the resource template, I am going to use variables.

Virtual Network
  1. {
  2.   "apiVersion": "2015-05-01-preview",
  3.   "type": "Microsoft.Network/virtualNetworks",
  4.   "name": "[parameters('virtualNetworkName')]",
  5.   "location": "[resourceGroup().location]",
  6.   "dependsOn": [ ],
  7.   "tags":
  8.   {
  9.     "displayName": "VirtualNetwork"
  10.   },
  11.   "properties":
  12.   {
  13.     "addressSpace":
  14.     {
  15.       "addressPrefixes":
  16.       [
  17.         "[variables('VirtualNetworkPrefix')]"
  18.       ]
  19.     },
  20.     "subnets":
  21.     [
  22.       {
  23.         "name": "[variables('VirtualNetworkSubnet1Name')]",
  24.         "properties":
  25.         {
  26.           "addressPrefix": "[variables('VirtualNetworkSubnet1Prefix')]"
  27.         }
  28.       },
  29.       {
  30.         "name": "[variables('VirtualNetworkSubnet2Name')]",
  31.         "properties":
  32.         {
  33.           "addressPrefix": "[variables('VirtualNetworkSubnet2Prefix')]"
  34.         }
  35.       },
  36.       {
  37.         "name": "[variables('VirtualNetworkSubnet3Name')]",
  38.         "properties":
  39.         {
  40.           "addressPrefix": "[variables('VirtualNetworkSubnet3Prefix')]"
  41.         }
  42.       }
  43.     ]
  44.   }
  45. },

Each place where we’ve used the [variables()] syntax requires an accompanying variable to be declared.  We add those to the “variables” section.  This allows me to easily separate out the variables and promote them to input parameters later if I decide I want the user to be able to configure this value.

Variables
  1. "VirtualNetworkPrefix": "10.0.0.0/16",
  2. "VirtualNetworkSubnet1Name": "Subnet-1",
  3. "VirtualNetworkSubnet1Prefix": "10.0.0.0/24",
  4. "VirtualNetworkSubnet2Name": "Subnet-2",
  5. "VirtualNetworkSubnet2Prefix": "10.0.1.0/24",
  6. "VirtualNetworkSubnet3Name": "Subnet-3",
  7. "VirtualNetworkSubnet3Prefix": "10.0.2.0/24",

NetworkInterface

The NetworkInterface binds a virtual machine to a subnet.  If we want to place a VM in Subnet-1, we need to create a network interface to map the two together.  If you examine the networkInterface resource, you will see that it uses the copy element with the copyindex() function.

image

This allows us to perform rudimentary looping to create multiple resources.  The end user provides a parameter of how many instances they want, and we create that many networkInterfaces.  If the user provides the name “NetworkInterface” with 3 instances, the output would be “NetworkInterface1”, “NetworkInterface2”, and “NetworkInterface3”.  I don’t want the user to have to provide this name, we’ll turn this into a variable.  However, I want to use this for the dev environment, but I don’t want to hard-code the environment name “dev” into the resource definition.  Instead, I add a variable “devPrefix” and concatenate its value with “nic” and the copyindex.

NetworkInterfaces
  1. {
  2.   "apiVersion": "2015-05-01-preview",
  3.   "type": "Microsoft.Network/networkInterfaces",
  4.   "name": "[concat(variables('devPrefix'), 'nic', copyindex())]",
  5.   "location": "[resourceGroup().location]",
  6.   "tags":
  7.   {
  8.     "displayName": "DevNetworkInterfaces"
  9.   },
  10.   "copy":
  11.   {
  12.     "name": "nicLoop",
  13.     "count": "[variables('numberOfInstances')]"
  14.   },
  15.   "dependsOn":
  16.   [
  17.     "[concat('Microsoft.Network/virtualNetworks/', parameters('virtualNetworkName'))]"
  18.   ],
  19.   "properties":
  20.   {
  21.     "ipConfigurations":
  22.     [
  23.       {
  24.         "name": "ipconfig1",
  25.         "properties":
  26.         {
  27.           "privateIPAllocationMethod": "Dynamic",
  28.           "subnet":
  29.           {
  30.             "id": "[variables('subnet1Ref')]"
  31.           }
  32.         }
  33.       }
  34.     ]
  35.  
  36.   }
  37. },

If the user input 3 instances with a prefix “Dev”, the output would now be “Dev1nic”,”Dev2nic”, and “Dev3nic”.

Virtual Machines

After we edit the network interfaces, we have more red marks in the margin.  This is telling us that we need to edit the virtual machines.  The first mark is in the dependsOn section.  We edit to use the same naming scheme that we just used for our network interface.

image

We do the same for the networkInterfaces section.

image

The virtual machines are named with a single prefix.  If we want three environments (dev, stage, prod), we have to change this from a single prefix to 3 different prefixes.  Delete the parameter named “vmNamePrefix” and use the variable “devPrefix” that we introduced previously.

image

image

Update the tag to reflect this is the development environment.

image

Finally, we have to specify the name of the OS disk.  Since we are using the copyindex() function to differentiate disks, we need to also include the environment name.

OSDisk
  1. "osDisk":
  2. {
  3.   "name": "osdisk",
  4.   "vhd":
  5.   {
  6.     "uri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/vhds/','osdisk', variables('devPrefix'), copyindex(), '.vhd')]"
  7.   },
  8.   "caching": "ReadWrite",
  9.   "createOption": "FromImage"
  10. }

The result is the following section.

DevVirtualMachines
  1. {
  2.       "apiVersion": "2015-05-01-preview",
  3.       "type": "Microsoft.Compute/virtualMachines",
  4.       "name": "[concat(variables('devPrefix'), copyindex())]",
  5.       "copy":
  6.       {
  7.         "name": "virtualMachineLoop",
  8.         "count": "[variables('numberOfInstances')]"
  9.       },
  10.       "location": "[resourceGroup().location]",
  11.       "tags":
  12.       {
  13.         "displayName": "DevVirtualMachines"
  14.       },
  15.       "dependsOn":
  16.       [
  17.         "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]",
  18.         "[concat('Microsoft.Network/networkInterfaces/', variables('devPrefix'), 'nic', copyindex())]",
  19.         "[concat('Microsoft.Compute/availabilitySets/', variables('devAvailabilitySetName'))]"
  20.       ],
  21.       "properties":
  22.       {
  23.         "availabilitySet":
  24.         {
  25.           "id": "[resourceId('Microsoft.Compute/availabilitySets',variables('devAvailabilitySetName'))]"
  26.         },
  27.         "hardwareProfile":
  28.         {
  29.           "vmSize": "[parameters('vmSize')]"
  30.         },
  31.         "osProfile":
  32.         {
  33.           "computername": "[concat(variables('devPrefix'), copyIndex())]",
  34.           "adminUsername": "[parameters('adminUsername')]",
  35.           "adminPassword": "[parameters('adminPassword')]"
  36.         },
  37.         "storageProfile":
  38.         {
  39.           "imageReference":
  40.           {
  41.             "publisher": "[parameters('imagePublisher')]",
  42.             "offer": "[parameters('imageOffer')]",
  43.             "sku": "[parameters('imageSKU')]",
  44.             "version": "latest"
  45.           },
  46.           "osDisk":
  47.           {
  48.             "name": "osdisk",
  49.             "vhd":
  50.             {
  51.               "uri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/vhds/','osdisk', variables('devPrefix'), copyindex(), '.vhd')]"
  52.             },
  53.             "caching": "ReadWrite",
  54.             "createOption": "FromImage"
  55.           }
  56.         },
  57.         "networkProfile":
  58.         {
  59.           "networkInterfaces":
  60.           [
  61.             {
  62.               "id": "[resourceId('Microsoft.Network/networkInterfaces',concat(variables('devPrefix'), 'nic', copyindex()))]"
  63.             }
  64.           ]
  65.         }
  66.       }
  67.     }

After all that work, you are now back to pretty much where you started, but with a few improvements.

You Now Have One Environment

The result is now a little more evident in the resources node in the JSON Outline pane.  Instead of just one environment, we now are set up for a Dev environment.

image

While it may seem that we are no better off than before, you will see that we can now more quickly duplicate sections and create the stage and prod environments.

Staging Environment

I could easily right-click the “resources” node in the JSON Outline and choose “Add New Resource”.  That would add the resource, variables, and parameters to support the newly added resource.  However, we would just have to go back and edit the parameters and variables, so we are going to edit the JSON by hand.  Don’t worry, it’s not that bad.

AvailabilitySet

Click on the “DevAvailabilitySet” node in the JSON Outline.  The entire JSON block is highlighted.  Collapse the DevVirtualMachines node so that you can easily see where to paste a copy.

image

Paste the following to define an availability set for the staging environment.

StageAvailabilitySet
  1. {
  2.   "apiVersion": "2015-05-01-preview",
  3.   "type": "Microsoft.Compute/availabilitySets",
  4.   "name": "[variables('stageAvailabilitySetName')]",
  5.   "location": "[resourceGroup().location]",
  6.   "tags":
  7.   {
  8.     "displayName": "StageAvailabilitySet"
  9.   }
  10. },

We get a red mark in the margin because we reference a variable, “stageAvailabilitySetName”, that doesn’t yet exist.  Just add that to the variables section to fix it.

image

NetworkInterfaces

 

Now we copy the DevNetworkInterfaces section and paste below the StageAvailabilitySet section.

StageNetworkInterface
  1. {
  2.   "apiVersion": "2015-05-01-preview",
  3.   "type": "Microsoft.Network/networkInterfaces",
  4.   "name": "[concat(variables('stagePrefix'), 'nic', copyindex())]",
  5.   "location": "[resourceGroup().location]",
  6.   "tags":
  7.   {
  8.     "displayName": "StageNetworkInterfaces"
  9.   },
  10.   "copy":
  11.   {
  12.     "name": "nicLoop",
  13.     "count": "[variables('numberOfInstances')]"
  14.   },
  15.   "dependsOn":
  16.   [
  17.     "[concat('Microsoft.Network/virtualNetworks/', parameters('virtualNetworkName'))]"
  18.   ],
  19.   "properties":
  20.   {
  21.     "ipConfigurations":
  22.     [
  23.       {
  24.         "name": "ipconfig1",
  25.         "properties":
  26.         {
  27.           "privateIPAllocationMethod": "Dynamic",
  28.           "subnet":
  29.           {
  30.             "id": "[variables('subnet2Ref')]"
  31.           }
  32.         }
  33.       }
  34.     ]
  35.  
  36.   }
  37. },

Notice on line 30 that we are putting these VMs in subnet2, just like in the diagram at the beginning of the post.  We also introduce a new variable, “stagePrefix” with a value of “stage”.

VirtualMachines

We’ve already done the hard work to parameterize all of this, so now we can copy the virtual machines section and replace “dev” with “stage”.

VirtualMachines
  1. {
  2.       "apiVersion": "2015-05-01-preview",
  3.       "type": "Microsoft.Compute/virtualMachines",
  4.       "name": "[concat(variables('stagePrefix'), copyindex())]",
  5.       "copy":
  6.       {
  7.         "name": "virtualMachineLoop",
  8.         "count": "[variables('numberOfInstances')]"
  9.       },
  10.       "location": "[resourceGroup().location]",
  11.       "tags":
  12.       {
  13.         "displayName": "StageVirtualMachines"
  14.       },
  15.       "dependsOn":
  16.       [
  17.         "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]",
  18.         "[concat('Microsoft.Network/networkInterfaces/', variables('stagePrefix'), 'nic', copyindex())]",
  19.         "[concat('Microsoft.Compute/availabilitySets/', variables('stageAvailabilitySetName'))]"
  20.       ],
  21.       "properties":
  22.       {
  23.         "availabilitySet":
  24.         {
  25.           "id": "[resourceId('Microsoft.Compute/availabilitySets',variables('stageAvailabilitySetName'))]"
  26.         },
  27.         "hardwareProfile":
  28.         {
  29.           "vmSize": "[parameters('vmSize')]"
  30.         },
  31.         "osProfile":
  32.         {
  33.           "computername": "[concat(variables('stagePrefix'), copyIndex())]",
  34.           "adminUsername": "[parameters('adminUsername')]",
  35.           "adminPassword": "[parameters('adminPassword')]"
  36.         },
  37.         "storageProfile":
  38.         {
  39.           "imageReference":
  40.           {
  41.             "publisher": "[parameters('imagePublisher')]",
  42.             "offer": "[parameters('imageOffer')]",
  43.             "sku": "[parameters('imageSKU')]",
  44.             "version": "latest"
  45.           },
  46.           "osDisk":
  47.           {
  48.             "name": "osdisk",
  49.             "vhd":
  50.             {
  51.               "uri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/vhds/','osdisk', variables('stagePrefix'), copyindex(), '.vhd')]"
  52.             },
  53.             "caching": "ReadWrite",
  54.             "createOption": "FromImage"
  55.           }
  56.         },
  57.         "networkProfile":
  58.         {
  59.           "networkInterfaces":
  60.           [
  61.             {
  62.               "id": "[resourceId('Microsoft.Network/networkInterfaces',concat(variables('stagePrefix'), 'nic', copyindex()))]"
  63.             }
  64.           ]
  65.         }
  66.       }
  67.     }

Prod Environment

We did that so quick this time!  We can now just copy the sections we just pasted and replace “stage” with “prod”.  The easiest way to do this is to leverage the editor… just collapse the 3 sections we previously created.

image

Paste.  Now select the three sections you just pasted.  Use Ctrl+H to find and replace…

image

The result is our set of resources describing the environments we want to deploy.

image

Testing Things Out

Let’s test things out.  When we created the project, a file called “LoadBalancedVirtualMachine.param.dev.json” was created.  Let’s use that file to provide the parameter values for our script.

image

Params
  1. {
  2.   "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  3.   "contentVersion": "1.0.0.0",
  4.   "parameters":
  5.   {
  6.     "virtualNetworkName":
  7.     {
  8.       "value": "kirkermvnet"
  9.     },
  10.     "adminUsername":
  11.     {
  12.       "value": "myadmin"
  13.  
  14.     }
  15.   }
  16. }

Notice we don’t provide a value for every parameter because some have default values.

Right-click on the “Deploy-AzureResourceGroup.ps1” script and choose “Open with PowerShell ISE”.

image

In the PowerShell ISE command window, execute “Switch-AzureMode AzureResourceManager” and then “Add-AzureAccount”.

image

Now run the script.

Debugging

And if you followed along to this point, you probably got the same outcome as me… everything was provisioned correctly except the virtual machines.

image

There are a few things you can do here, such as go to the portal and look at the logs for the resource group.

Get-AzureResourceGroupLog
  1. Get-AzureResourceGroupLog -ResourceGroup DevTestProd -DetailedOutput

We can also go to the resource group and click on the last deployment date.  From there we can see the parameters used to deploy the template.

image

Scroll down and you can see the list of operations

image

Click on an operation to get the details.  This is the same information as what you pulled from the logs in PowerShell.  More details are available at Troubleshooting deployments.

Truth be told, this screenshot shows Status=OK, but I was staring at “Bad Request” for quite awhile without any helpful information beyond that.  Troubleshooting templates can be frustrating when you have little information to go on (and have been editing JSON directly for the past few hours), but trust me… this is worth troubleshooting through.

image

After a few hours of pulling my hair out and not getting anything beyond “Bad Request”, I finally thought to use a password stronger than “pass@word1”.  I’ll be darned, it worked.  Not only that, but provisioning with Azure Resource Manager is asynchronous, so your scripts finish a heck of a lot sooner than they used to because VMs provision in parallel.

image

We can go to the new portal and see all of our stuff.  For instance, we can inspect the virtual network in the resource group and confirm the VMs are allocated to different subnets.

image

More important, we can now go tag our resources and those tags show up in billing, and we can now use Role Based Access Control (RBAC) to control access to resources.  This is so much better than adding everyone as a subscription admin.

image

Download the Code

The template for this post is available on GitHub at https://github.com/kaevans/DevTestProd.  Something to call out is that I added a link that will let you import the JSON template to your subscription.

image

When you click the link, you are taken to the Azure portal where you can import the template.

image

Save, and now you can provide parameters using the portal.

image

You could also go to the Marketplace and search for Template Deployment.

image

image

Once you create the template deployment, you can edit the template.

image

 

For More Information

Download the code – https://github.com/kaevans/DevTestProd

Gallery of ARM templates – https://github.com/Azure/azure-quickstart-templates

Azure Resource Manager Overview

Using Azure PowerShell with Resource Manager

Using the Azure CLI with Resource Manager

Using the Azure Portal to manage resources

Authoring templates

Troubleshooting deployments