Creating ARM Templates with Azure Resource Explorer

This post will show how to use Azure Resource Explorer to assist in creating Azure Resource Manager templates.

Background

My team at Microsoft is busy on the road delivering a series of workshops to our top partners around the world.  One of the sessions that I put together for the workshop is called “Architecting Global Scale Solutions”, for which I created a web application.  I really wanted to make it easy for other presenters to deliver the session so I decided to create an ARM template that could deploy the solution to their subscription.  Doing this in a single region was simple, but doing this for a variable number of regions took a bit more creativity.

I am going to walk through the process of how I created my template, and point out the bumps along the way so that, hopefully, you will avoid those same bumps.

Dive Right In… to the Documentation

I like to start coding something first, see how far I can get, and then go read about it.  My first approach to ARM templates was to just dive right in and see what I could do.  The result was, well, ugly to say the least.  It worked, but I could just see from the way that I created the template that there was a better way to structure the template and avoid the redundancy.  Measure once, cut twice.

I needed to spend some more time with the documentation to understand what was going on.  The first bit of documentation you should spend time with is Azure Resource Manager template functions.  That article shows the various functions that ARM provides in a template.  Next you should understand how to create multiple instances of resources in Azure Resource Manager.  Finally, you want to spend some time examining how others have approached scenarios with ARM templates.  There is a fantastic article, Provision and deploy microservices predictably in Azure that I really wish I had read before I started on my current project as it would have saved me a huge amount of time.  There are also great examples on GitHub, such as David Ebbo’s AzureWebsitesSamples repository.

A Quick Example

For instance, suppose that you want to let the user specify a variable number of regions to deploy a storage account to.  First, you need is a parameter that captures the input as an array.  Second, you probably want to set some variables for things that possibly change in the future.  You can also use variables to make your resource templates a little less cluttered.  Finally, you need a way to iterate over the items in the array.  Here is an example of just that that I cobbled together from all the various resources above.

Create Storage

Let’s use some of our newfound knowledge to create storage accounts in multiple regions.

Storage in Multiple Regions

  1. {
  2.   "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  3.   "contentVersion": "1.0.0.0",
  4.   "parameters":
  5.   {
  6.     "siteLocations":
  7.     {
  8.       "type": "array",
  9.       "defaultValue":
  10.       [
  11.         "West Europe",
  12.         "East Asia",
  13.         "Southeast Asia",
  14.         "East US",
  15.         "West US"
  16.       ],
  17.       "metadata":
  18.       {
  19.         "description": "Locations"
  20.       }
  21.     }
  22.   },
  23.   "variables":
  24.   {
  25.     "StorageName": "[concat('Storage', uniqueString(resourceGroup().id))]",
  26.     "StorageType": "Standard_LRS"
  27.   },
  28.   "resources":
  29.   [
  30.     {
  31.       "name": "[concat(variables('StorageName'),copyIndex())]",
  32.       "type": "Microsoft.Storage/storageAccounts",
  33.       "location": "[parameters('siteLocations')[copyIndex()]]",
  34.       "apiVersion": "2015-05-01-preview",
  35.       "dependsOn":
  36.       [
  37.       ],
  38.       "tags":
  39.       {
  40.         "displayName": "Storage"
  41.       },
  42.       "properties":
  43.       {
  44.         "accountType": "[variables('StorageType')]"
  45.       },
  46.       "copy":
  47.       {
  48.         "name": "storageCopy",
  49.         "count": "[length(parameters('siteLocations'))]"
  50.       }
  51.     }
  52.   ]
  53. }

The “StorageName” variable uses several functions together.  First, it finds the ID of the resource group and passes that into the uniqueString function.  It then concatenates the prefix “Storage” with the string, resulting in a value of “Storagestkfws5lccb7u”… very likely a unique storage account name.  While this template creates a standard locally redundant storage account (“Standard_LRS”), someone else who uses the template might want to change that later.

Notice on line 44 that we add the copy property, telling it to make as many of these as there are items in the “siteLocations” parameter.  When we provision the resource, we use the copyIndex() function to specify which iteration we are currently on. 

Test it Out

If you are using Visual Studio to author the template (making your life SO much easier), you could create an Azure Resource Group project, copy this JSON into the document, right-click the project and choose deploy.  Alternatively, in the Azure Portal, create a new Template Deployment and paste the template in there.

image

image

image

If we just leave the defaults, creating them in a resource group named “demogroup”, this results in the following storage accounts:

Storage Account Name Region Type
Storagevl3bsgu3ajzku0 West Europe Standard_LRS
Storagevl3bsgu3ajzku1 East Asia Standard_LRS
Storagevl3bsgu3ajzku2 Southeast Asia Standard_LRS
Storagevl3bsgu3ajzku3 East US Standard_LRS
Storagevl3bsgu3ajzku4 West US Standard_LRS

If you go to the resource group and view the resources, you can confirm this.

image

Add a Web App

Now let’s add a web app to the template.  I am going to use Visual Studio for this one as it makes the authoring a whole lot easier.  I create a new Azure Resource Group project, and paste my template in the DeploymentTemplate.JSON file.  Next, go to the JSON Outline tab and you’ll be able to see the outline of your document.

image

Right-click on the resources node and add a new web app.  Notice that it asks you for an App Service plan, prompting you to create one before you can create the web app.

image

image

Here is what is generated.  It’s not right yet, but that’s just because Visual Studio couldn’t read our minds.

The Wrong Stuff

  1. {
  2.   "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  3.   "contentVersion": "1.0.0.0",
  4.   "parameters":
  5.   {
  6.     "siteLocations":
  7.     {
  8.       "type": "array",
  9.       "defaultValue":
  10.       [
  11.         "West Europe",
  12.         "East Asia",
  13.         "Southeast Asia",
  14.         "East US",
  15.         "West US"
  16.       ],
  17.       "metadata":
  18.       {
  19.         "description": "Locations"
  20.       }
  21.     },
  22.     "HostingPlanName":
  23.     {
  24.       "type": "string",
  25.       "minLength": 1
  26.     },
  27.     "HostingPlanSKU":
  28.     {
  29.       "type": "string",
  30.       "allowedValues":
  31.       [
  32.         "Free",
  33.         "Shared",
  34.         "Basic",
  35.         "Standard"
  36.       ],
  37.       "defaultValue": "Free"
  38.     },
  39.     "HostingPlanWorkerSize":
  40.     {
  41.       "type": "string",
  42.       "allowedValues":
  43.       [
  44.         "0",
  45.         "1",
  46.         "2"
  47.       ],
  48.       "defaultValue": "0"
  49.     }
  50.   },
  51.   "variables":
  52.   {
  53.     "StorageName": "[concat('Storage', uniqueString(resourceGroup().id))]",
  54.     "StorageType": "Standard_LRS",
  55.     "WebSiteName": "[concat('WebSite', uniqueString(resourceGroup().id))]"
  56.   },
  57.   "resources":
  58.   [
  59.     {
  60.       "name": "[concat(variables('StorageName'),copyIndex())]",
  61.       "type": "Microsoft.Storage/storageAccounts",
  62.       "location": "[parameters('siteLocations')[copyIndex()]]",
  63.       "apiVersion": "2015-05-01-preview",
  64.       "dependsOn":
  65.       [
  66.       ],
  67.       "tags":
  68.       {
  69.         "displayName": "Storage"
  70.       },
  71.       "properties":
  72.       {
  73.         "accountType": "[variables('StorageType')]"
  74.       },
  75.       "copy":
  76.       {
  77.         "name": "storageCopy",
  78.         "count": "[length(parameters('siteLocations'))]"
  79.       }
  80.     },
  81.     {
  82.       "name": "[parameters('HostingPlanName')]",
  83.       "type": "Microsoft.Web/serverfarms",
  84.       "location": "[resourceGroup().location]",
  85.       "apiVersion": "2014-06-01",
  86.       "dependsOn":
  87.       [
  88.       ],
  89.       "tags":
  90.       {
  91.         "displayName": "HostingPlan"
  92.       },
  93.       "properties":
  94.       {
  95.         "name": "[parameters('HostingPlanName')]",
  96.         "sku": "[parameters('HostingPlanSKU')]",
  97.         "workerSize": "[parameters('HostingPlanWorkerSize')]",
  98.         "numberOfWorkers": 1
  99.       }
  100.     },
  101.     {
  102.       "name": "[variables('WebSiteName')]",
  103.       "type": "Microsoft.Web/sites",
  104.       "location": "[resourceGroup().location]",
  105.       "apiVersion": "2014-06-01",
  106.       "dependsOn":
  107.       [
  108.         "[concat('Microsoft.Web/serverfarms/', parameters('HostingPlanName'))]"
  109.       ],
  110.       "tags":
  111.       {
  112.         "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('HostingPlanName'))]": "Resource",
  113.         "displayName": "WebSite"
  114.       },
  115.       "properties":
  116.       {
  117.         "name": "[variables('WebSiteName')]",
  118.         "serverFarm": "[parameters('HostingPlanName')]"
  119.       }
  120.     }
  121.   ]
  122. }

So what’s wrong with it?  First, it added a few parameters that we don’t really need to expose to the user.  Second, it is only going to create one web app and put it in the same region as the resource group.  Let’s make a few edits. We will move those parameter values into variables.  Second, we’ll use the same copyIndex() function trick we used before to name the resources.

A Little Better

  1. {
  2.   "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  3.   "contentVersion": "1.0.0.0",
  4.   "parameters":
  5.   {
  6.     "siteLocations":
  7.     {
  8.       "type": "array",
  9.       "defaultValue":
  10.       [
  11.         "West Europe",
  12.         "East Asia",
  13.         "Southeast Asia",
  14.         "East US",
  15.         "West US"
  16.       ],
  17.       "metadata":
  18.       {
  19.         "description": "Locations"
  20.       }
  21.     }    
  22.   },
  23.   "variables":
  24.   {
  25.     "StorageName": "[concat('Storage', uniqueString(resourceGroup().id))]",
  26.     "StorageType": "Standard_LRS",    
  27.     "HostingPlanName": "[concat('Plan', uniqueString(resourceGroup().id))]",
  28.     "HostingPlanSKU": "Standard",
  29.     "HostingPlanWorkerSize": "0",
  30.     "WebSiteName": "[concat('WebSite', uniqueString(resourceGroup().id))]",   
  31.   },
  32.   "resources":
  33.   [
  34.     {
  35.       "name": "[concat(variables('StorageName'),copyIndex())]",
  36.       "type": "Microsoft.Storage/storageAccounts",
  37.       "location": "[parameters('siteLocations')[copyIndex()]]",
  38.       "apiVersion": "2015-05-01-preview",
  39.       "dependsOn":
  40.       [
  41.       ],
  42.       "tags":
  43.       {
  44.         "displayName": "Storage"
  45.       },
  46.       "properties":
  47.       {
  48.         "accountType": "[variables('StorageType')]"
  49.       },
  50.       "copy":
  51.       {
  52.         "name": "storageCopy",
  53.         "count": "[length(parameters('siteLocations'))]"
  54.       }
  55.     },
  56.     {
  57.       "name": "[concat(variables('HostingPlanName'),copyIndex())]",
  58.       "type": "Microsoft.Web/serverfarms",
  59.       "location": "[parameters('siteLocations')[copyIndex()]]",
  60.       "apiVersion": "2014-06-01",
  61.       "dependsOn":
  62.       [
  63.       ],
  64.       "tags":
  65.       {
  66.         "displayName": "HostingPlan"
  67.       },
  68.       "properties":
  69.       {
  70.         "name": "[concat(variables('HostingPlanName'),copyIndex())]",
  71.         "sku": "[variables('HostingPlanSKU')]",
  72.         "workerSize": "[variables('HostingPlanWorkerSize')]",
  73.         "numberOfWorkers": 1
  74.       },
  75.       "copy":
  76.       {
  77.         "name": "farmCopy",
  78.         "count": "[length(parameters('siteLocations'))]"
  79.       }
  80.     },
  81.     {
  82.       "name": "[concat(variables('WebSiteName'),copyIndex())]",
  83.       "type": "Microsoft.Web/sites",
  84.       "location": "[parameters('siteLocations')[copyIndex()]]",
  85.       "apiVersion": "2014-06-01",
  86.       "dependsOn":
  87.       [
  88.         "[concat('Microsoft.Web/serverfarms/', variables('HostingPlanName'), copyIndex())]"
  89.       ],
  90.       "tags":
  91.       {
  92.         "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', variables('HostingPlanName'), copyIndex())]": "Resource",
  93.         "displayName": "WebSite"
  94.       },
  95.       "properties":
  96.       {
  97.         "name": "[concat(variables('WebSiteName'),copyIndex())]",
  98.         "serverFarm": "[concat(variables('HostingPlanName'),copyIndex())]"
  99.       },
  100.       "copy":
  101.       {
  102.         "name": "webCopy",
  103.         "count": "[length(parameters('siteLocations'))]"
  104.       }
  105.     }
  106.   ]
  107. }

The changes aren’t that hard to read, but it’s not what I would have come up with on my first try at authoring a template.  It helps to see how others approach the problem.

Test it Out

In Visual Studio, right-click the project node and choose deploy.  Choose the group that we created before in the portal. 

image

Of course you don’t have to create the resource group in the portal first, I am just showing that you could build on top of an existing deployment.  As the template is deployed, you can watch the status in the output window.

image

ARM deployments are idempotent.  The storage accounts already existed, so Azure Resource Manager skipped provisioning them.  You can see that looking at the time stamps, all of them within a few seconds of starting the deployment.  The web apps are added to the existing resource group.  You can switch to Cloud Explorer in Visual Studio to view the deployed resources.

image

Azure Resource Explorer

Here’s where I started to get stuck.  I have a web application and I have a storage account.  I need to update the appSettings for each web app to point to the storage connection string, and I need to change the web app to use the Always On setting.  Looking through the resources above, I found 3 different ways to do this, and none of them matched the schema version (apiVersion = “2014-06-01”) that Visual Studio used.  If I change the schema version, what else will break?  How in the world do I figure out how to add appSettings?  The easiest way is to just go to one of those deployed web applications and set the appSetting that you need.

image

Now navigate over to https://resources.azure.com.  Sign in, and you’ll be able to navigate through the resources that you’ve already provisioned, and we can navigate down to the web app and see its alwaysOn setting. 

image

We can also see the appSettings value we just entered by going to the appSettings node.

image

That’s cool, and it gives us a hint of where to look, but it doesn’t tell us exactly how to author the template.  Let me walk you through it.

Notice that it is using the latest schema, at the time of this writing it is version “2015-08-01”, to query the resource.  I was having problems making heads or tails of what I was supposed to do next.  Do I change my template to use that schema?  Visual Studio was telling me that was an invalid schema number… is it OK to ignore that warning? 

I tried creating the appSettings section without adding a schema version, and the deployment failed.

image

I added the schema version “2015-08-01”, and deployed again.  This time, a different error. 

06:40:24 - [ERROR] New-AzureResourceGroup : Error 1: Code=InvalidTemplate; Message=Deployment
06:40:24 - [ERROR] template validation failed: 'The template resource 'appsettings' for type
06:40:24 - [ERROR] 'Microsoft.Web/sites/config' at line '0' and column '0' has incorrect segment
06:40:24 - [ERROR] lengths. A nested resource type must have identical number of segments as its
06:40:24 - [ERROR] resource name. A root resource type must have segment length one greater than
06:40:24 - [ERROR] its resource name.'.

After some searching, I realized that the error was very specific (but yet still difficult to understand).  It was saying that the type property “Microsoft.Web/sites/config” (notice there are 3 segments) needs to match the same number of segments as it name (notice there is just 1 segment).  The fix, then, was to change the type to simply “config”.  That wasn’t shown in the Resource Explorer. 

This time I received a different error.

06:44:49 - [ERROR] New-AzureResourceGroup : Error 1: Code=InvalidTemplate; Message=Deployment
06:44:49 - [ERROR] template validation failed: 'The resource '/subscriptions/f610c69d-76f7-4ff1-a9
06:44:49 - [ERROR] ed-0f34863459e7/resourceGroups/demogroup/providers/Microsoft.Web/sites/WebSitev
06:44:49 - [ERROR] l3bsgu3ajzku0/config/appsettings' at line '0' and column '0' doesn't depend on
06:44:49 - [ERROR] parent resource '/subscriptions/f610c69d-76f7-4ff1-a9ed-0f34863459e7/resourceGr
06:44:49 - [ERROR] oups/demogroup/providers/Microsoft.Web/sites/WebSitevl3bsgu3ajzku0'. Please
06:44:49 - [ERROR] add dependency explicitly using the 'dependsOn' syntax.'.

This one is saying that I need to explicitly create a dependency between by appSettings element and the parent.  That wasn’t shown in the Resource Explorer, either.

Adding AppSettings

  1. "resources":
  2.       [
  3.         {
  4.           "apiVersion": "2015-08-01",
  5.           "name": "appsettings",
  6.           "type": "config",
  7.           "dependsOn":
  8.           [
  9.             "[resourceId('Microsoft.Web/sites', concat(variables('WebSiteName'),copyIndex()))]"
  10.           ],
  11.           "properties":
  12.           {
  13.             "foo": "bar"
  14.           }
  15.         }
  16.       ]

Deploy again, and it only takes a second or two to complete.  Our appSetting is now in the other 4 web sites.

image

Once I got it to work, I scrolled up in Visual Studio, and notice that it is prompting me to update the schema version for the web app.

image

Just like I said before, I like to try things, get a little stuck, then hopefully find something in docs or on StackOverflow that shows me the errors of my ways.  I read David Ebbo’s sample, WebAppManyFeatures.json, and I saw that he had updated his schema version as well.  I updated mine and everything worked like a champ.  I could have saved myself a bit of grief if I had started by looking at someone else’s success to begin with.

Now how about the storage connection string?  It would be nice to wire those two together.  Remember when I said that we should understand the Azure Resource Manager template functions?  Here is an example of what I mean.

Storage in Multiple Regions

  1. [
  2.         {
  3.           "apiVersion": "2015-08-01",
  4.           "name": "appsettings",
  5.           "type": "config",
  6.           "dependsOn":
  7.           [
  8.             "[resourceId('Microsoft.Web/sites', concat(variables('WebSiteName'),copyIndex()))]"
  9.           ],
  10.           "properties":
  11.           {
  12.             "foo": "bar",
  13.             "storageConnectionString": "[concat('DefaultEndpointsProtocol=https;AccountName=',toLower(concat(variables('StorageName'), copyIndex())),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts',concat(variables('StorageName'), copyIndex())),'2015-05-01-preview').key1)]"
  14.           }
  15.         }
  16.       ]

We use the listKeys() function to get the key.  To form the entire connection string, we have to do a bit of string manipulation using the concat() function.  Deploy one last time, and now each web site points to its own storage account in its region.  No manual intervention required for deployment, and we get a consistent deployment in every single region. 

image

Wrapping It Up

The point to all of this is that there is a great resource, Azure Resource Explorer, to help you figure out the settings for your templates.  Just go to the new portal, make the desired change, then find the change in the explorer and include it in your template.  However, it’s not always that straightforward, you are going to need some trial and error.  Hopefully the docs will catch up to the innovation at some point to take a bit of the guesswork out of the creation process.

Resources

Azure Resource Explorer

Azure Resource Manager Templates with Visual Studio 2015

Azure Resource Manager template functions

Provision and deploy microservices predictably in Azure

David Ebbo’s AzureWebsitesSamples repository