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.
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.
@kaevans you can pretty much do everything in mentioned in the post using ARM template language and more with VM extensions
— Ram (@iotdude) June 6, 2015
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.
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.
Clicking that will create the following:
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.
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.
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”.
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.
Easy enough, I don’t need a load balancer, so I delete that line of code.
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”.
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”.
We now have red marks in the margin.
Scroll down to the red marks and correct the errors.
- {
- "apiVersion": "2015-05-01-preview",
- "type": "Microsoft.Compute/availabilitySets",
- "name": "[variables('devAvailabilitySetName')]",
- "location": "[resourceGroup().location]",
- "tags":
- {
- "displayName": "DevAvailabilitySet"
- }
- },
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.
- {
- "apiVersion": "2015-05-01-preview",
- "type": "Microsoft.Network/virtualNetworks",
- "name": "[parameters('virtualNetworkName')]",
- "location": "[resourceGroup().location]",
- "dependsOn": [ ],
- "tags":
- {
- "displayName": "VirtualNetwork"
- },
- "properties":
- {
- "addressSpace":
- {
- "addressPrefixes":
- [
- "[variables('VirtualNetworkPrefix')]"
- ]
- },
- "subnets":
- [
- {
- "name": "[variables('VirtualNetworkSubnet1Name')]",
- "properties":
- {
- "addressPrefix": "[variables('VirtualNetworkSubnet1Prefix')]"
- }
- },
- {
- "name": "[variables('VirtualNetworkSubnet2Name')]",
- "properties":
- {
- "addressPrefix": "[variables('VirtualNetworkSubnet2Prefix')]"
- }
- },
- {
- "name": "[variables('VirtualNetworkSubnet3Name')]",
- "properties":
- {
- "addressPrefix": "[variables('VirtualNetworkSubnet3Prefix')]"
- }
- }
- ]
- }
- },
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.
- "VirtualNetworkPrefix": "10.0.0.0/16",
- "VirtualNetworkSubnet1Name": "Subnet-1",
- "VirtualNetworkSubnet1Prefix": "10.0.0.0/24",
- "VirtualNetworkSubnet2Name": "Subnet-2",
- "VirtualNetworkSubnet2Prefix": "10.0.1.0/24",
- "VirtualNetworkSubnet3Name": "Subnet-3",
- "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.
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.
- {
- "apiVersion": "2015-05-01-preview",
- "type": "Microsoft.Network/networkInterfaces",
- "name": "[concat(variables('devPrefix'), 'nic', copyindex())]",
- "location": "[resourceGroup().location]",
- "tags":
- {
- "displayName": "DevNetworkInterfaces"
- },
- "copy":
- {
- "name": "nicLoop",
- "count": "[variables('numberOfInstances')]"
- },
- "dependsOn":
- [
- "[concat('Microsoft.Network/virtualNetworks/', parameters('virtualNetworkName'))]"
- ],
- "properties":
- {
- "ipConfigurations":
- [
- {
- "name": "ipconfig1",
- "properties":
- {
- "privateIPAllocationMethod": "Dynamic",
- "subnet":
- {
- "id": "[variables('subnet1Ref')]"
- }
- }
- }
- ]
- }
- },
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.
We do the same for the networkInterfaces section.
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.
Update the tag to reflect this is the development environment.
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":
- {
- "name": "osdisk",
- "vhd":
- {
- "uri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/vhds/','osdisk', variables('devPrefix'), copyindex(), '.vhd')]"
- },
- "caching": "ReadWrite",
- "createOption": "FromImage"
- }
The result is the following section.
- {
- "apiVersion": "2015-05-01-preview",
- "type": "Microsoft.Compute/virtualMachines",
- "name": "[concat(variables('devPrefix'), copyindex())]",
- "copy":
- {
- "name": "virtualMachineLoop",
- "count": "[variables('numberOfInstances')]"
- },
- "location": "[resourceGroup().location]",
- "tags":
- {
- "displayName": "DevVirtualMachines"
- },
- "dependsOn":
- [
- "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]",
- "[concat('Microsoft.Network/networkInterfaces/', variables('devPrefix'), 'nic', copyindex())]",
- "[concat('Microsoft.Compute/availabilitySets/', variables('devAvailabilitySetName'))]"
- ],
- "properties":
- {
- "availabilitySet":
- {
- "id": "[resourceId('Microsoft.Compute/availabilitySets',variables('devAvailabilitySetName'))]"
- },
- "hardwareProfile":
- {
- "vmSize": "[parameters('vmSize')]"
- },
- "osProfile":
- {
- "computername": "[concat(variables('devPrefix'), copyIndex())]",
- "adminUsername": "[parameters('adminUsername')]",
- "adminPassword": "[parameters('adminPassword')]"
- },
- "storageProfile":
- {
- "imageReference":
- {
- "publisher": "[parameters('imagePublisher')]",
- "offer": "[parameters('imageOffer')]",
- "sku": "[parameters('imageSKU')]",
- "version": "latest"
- },
- "osDisk":
- {
- "name": "osdisk",
- "vhd":
- {
- "uri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/vhds/','osdisk', variables('devPrefix'), copyindex(), '.vhd')]"
- },
- "caching": "ReadWrite",
- "createOption": "FromImage"
- }
- },
- "networkProfile":
- {
- "networkInterfaces":
- [
- {
- "id": "[resourceId('Microsoft.Network/networkInterfaces',concat(variables('devPrefix'), 'nic', copyindex()))]"
- }
- ]
- }
- }
- }
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.
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.
Paste the following to define an availability set for the staging environment.
- {
- "apiVersion": "2015-05-01-preview",
- "type": "Microsoft.Compute/availabilitySets",
- "name": "[variables('stageAvailabilitySetName')]",
- "location": "[resourceGroup().location]",
- "tags":
- {
- "displayName": "StageAvailabilitySet"
- }
- },
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.
NetworkInterfaces
Now we copy the DevNetworkInterfaces section and paste below the StageAvailabilitySet section.
- {
- "apiVersion": "2015-05-01-preview",
- "type": "Microsoft.Network/networkInterfaces",
- "name": "[concat(variables('stagePrefix'), 'nic', copyindex())]",
- "location": "[resourceGroup().location]",
- "tags":
- {
- "displayName": "StageNetworkInterfaces"
- },
- "copy":
- {
- "name": "nicLoop",
- "count": "[variables('numberOfInstances')]"
- },
- "dependsOn":
- [
- "[concat('Microsoft.Network/virtualNetworks/', parameters('virtualNetworkName'))]"
- ],
- "properties":
- {
- "ipConfigurations":
- [
- {
- "name": "ipconfig1",
- "properties":
- {
- "privateIPAllocationMethod": "Dynamic",
- "subnet":
- {
- "id": "[variables('subnet2Ref')]"
- }
- }
- }
- ]
- }
- },
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”.
- {
- "apiVersion": "2015-05-01-preview",
- "type": "Microsoft.Compute/virtualMachines",
- "name": "[concat(variables('stagePrefix'), copyindex())]",
- "copy":
- {
- "name": "virtualMachineLoop",
- "count": "[variables('numberOfInstances')]"
- },
- "location": "[resourceGroup().location]",
- "tags":
- {
- "displayName": "StageVirtualMachines"
- },
- "dependsOn":
- [
- "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]",
- "[concat('Microsoft.Network/networkInterfaces/', variables('stagePrefix'), 'nic', copyindex())]",
- "[concat('Microsoft.Compute/availabilitySets/', variables('stageAvailabilitySetName'))]"
- ],
- "properties":
- {
- "availabilitySet":
- {
- "id": "[resourceId('Microsoft.Compute/availabilitySets',variables('stageAvailabilitySetName'))]"
- },
- "hardwareProfile":
- {
- "vmSize": "[parameters('vmSize')]"
- },
- "osProfile":
- {
- "computername": "[concat(variables('stagePrefix'), copyIndex())]",
- "adminUsername": "[parameters('adminUsername')]",
- "adminPassword": "[parameters('adminPassword')]"
- },
- "storageProfile":
- {
- "imageReference":
- {
- "publisher": "[parameters('imagePublisher')]",
- "offer": "[parameters('imageOffer')]",
- "sku": "[parameters('imageSKU')]",
- "version": "latest"
- },
- "osDisk":
- {
- "name": "osdisk",
- "vhd":
- {
- "uri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/vhds/','osdisk', variables('stagePrefix'), copyindex(), '.vhd')]"
- },
- "caching": "ReadWrite",
- "createOption": "FromImage"
- }
- },
- "networkProfile":
- {
- "networkInterfaces":
- [
- {
- "id": "[resourceId('Microsoft.Network/networkInterfaces',concat(variables('stagePrefix'), 'nic', copyindex()))]"
- }
- ]
- }
- }
- }
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.
Paste. Now select the three sections you just pasted. Use Ctrl+H to find and replace…
The result is our set of resources describing the environments we want to deploy.
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.
- {
- "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
- "contentVersion": "1.0.0.0",
- "parameters":
- {
- "virtualNetworkName":
- {
- "value": "kirkermvnet"
- },
- "adminUsername":
- {
- "value": "myadmin"
- }
- }
- }
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”.
In the PowerShell ISE command window, execute “Switch-AzureMode AzureResourceManager” and then “Add-AzureAccount”.
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.
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-ResourceGroupDevTestProd-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.
Scroll down and you can see the list of operations
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.
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.
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.
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.
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.
When you click the link, you are taken to the Azure portal where you can import the template.
Save, and now you can provide parameters using the portal.
You could also go to the Marketplace and search for Template Deployment.
Once you create the template deployment, you can edit the template.
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