Starting with Azure Resource Templates

14 minute read

Since that Azure uses the Azure Resource Manager you have the ability to setup your own templates for deploying your applications. This can be handy because the infrastructure for your application is typically made up of many components – maybe a virtual machine, storage account, and virtual network, or a web app, database, database server, and 3rd party services. You do not see these components as separate entities, instead you see them as related and interdependent parts of a single entity. These components can be deployed and managed as a group using the Azure Resource Manager. 

With the Resource Manager, you can create a simple template (in JSON format) that defines deployment and configuration of your application. This template is known as a Resource Manager template and provides a declarative way to define deployment. By using a template, you can repeatedly deploy your application throughout the app lifecycle and have confidence your resources are deployed in a consistent state.

Creating the Azure Resource Template

To start with a Azure Resource Template a new project of the template “Azure Resource Group” needs to be added to your Visual Studio Solution.

image

When clicking “Ok” a dialog will appear that allows you to pick a kind of resource for deployment. When the resource is chosen the project will be created. This will give a project with three folders:

  • Scripts: Folder for PowerShell scripts that are needed for the deployment. By default it will contain a default script for deploying your resource.
  • Templates: Folder for the Azure Resource Manager templates. By default it will contain the Resource Template it self and a parameters file.
  • Tools: Folder that contains tools that are needed for the deployment. By default it contains AzCopy.exe

Editing the Resource Template

Within Visual Studio there is a text editor for editing the resource template that basically is a large JSON file. This combined with the JSON Outline viewer gives you the flexibility to do many actions. Which actions you can perform depend on the API versions you use within the Resource Template and what actions are available. It could be possible that JSON Outline viewer does not container actions that the ARM API supports. When this occurs the actions need to be added manually.

From the JSON Outline viewer you can add additional resources to your template by clicking the right mouse button on the resources node.

image

Adding resources can also be done on another level. This will give you different options for example adding a a Web Deploy or Application Settings element to a Web Application.

image

Deploying a Azure API Application

Most of the time when building a Azure API Application it will not only consists of Azure App Services but also Application Insights and a Azure Storage account for example. To deploy these resources all at once the Azure Resource Manager is the way to go. But to get there you will have to keep a couple things in mind.

To help you with building templates make use of the Azure Resource Explorer:

In the Azure Resource Explorer you will find all your resources you have created within the portal all in JSON representations.

Setting the Web App Type

When selecting a resource for the ARM template there is no option for a API application. The way to create a API Application is by adding a “Web App” resource and setting a specific property to “api” within the template. This property is called “kind”.

"apiVersion": "2015-08-01",
"name": "[parameters('apiName')]",
"kind" :  "api",
"type": "Microsoft.Web/sites",
"location": "[resourceGroup().location]",
"tags": {
  "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource",
  "displayName": "Website"
},

Adding application settings

When developing web applications there will be a need for saving settings within the web.config. These settings can be overridden within Azure via the Application Settings blade. Example for settings are:

AlwaysOn en Client Certificates (not possible trough the interface)

"properties": {
  "name": "[parameters('apiName')]",
  "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]",
  "clientCertEnabled": true,
  "siteConfig": {
    "AlwaysOn" :  true
  }
}

Many other settings are depended on other resources like: Application Insights and Azure Storage Accounts. To reference specific values from other resources you can use a specific string concatenation but the resource needs to be created first. When the template is setup in the correct way Azure Resource Manager will handle this dependencies to other resources.

Storage Account Connection String

The storage account connection string can be created with the default endpoint and a key from the Storage Account. These properties can be retrieved and set as connection string by using the below JSON.

"properties": {
  "StorageConnectionString": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(variables('storageAccountName'),'2016-01-01').keys[0].value)]"
}

Application Insights Instrumentation Key

When making use of application insights the application has to know the instrumentation key in order to log the resources. An option for this is to register the instrumentation key in the web.config and register is it in the Application_Start event as you can read here: Application Insights configurable instrumentation key.

The instrumentation key is added to the web.config by setting the correct reference.

"properties": {
  "InstrumentationKey": "[reference(concat('Microsoft.Insights/components/', parameters('insightsName'))).InstrumentationKey]"
}

Deploying your Web Application

With Azure Resource Management templates there is also the option to deploy your application. This can be added to the template by adding the project you created as a reference and adding a “Web Deploy for Web Apps” resource.

To complete this option the PowerShell script and the template need to be altered. What you need to understand is that the zip file containing your sources has to be present within Azure in order to be able to deploy it to your web application. So you have to create a Storage account within Azure and add your zip package to the blob container.

With the package uploaded you need to add the ‘_artifactsLocation’ and ‘_artifactsLocationSasToken’ parameters to the resource provider in order for it to know where the deployment package is saved. I also altered the default PowerShell to append the date and time to the package name in order to save old deployment packages within the storage container.

The file name is specified in the ‘_packageFileName’ parameter that is also send to the resource provider by the use of the OptionalParameters. The complete files can be viewed in the following sections.

PowerShell deployment file

#Requires -Version 3.0
#Requires -Module AzureRM.Resources
#Requires -Module Azure.Storage

Param(
    [string] $ResourceGroupLocation = '[Location]',
    [string] $ResourceGroupName = '[Resource Group Name',
    [string] $WebApiPackage = '[Full path to pacakge]',
    [string] $TemplateFile = '[Path to the template]',
    [string] $TemplateParametersFile = 'Path to the parameters file]',
    [string] $DeployContainer = 'deployment' 
)

Import-Module Azure -ErrorAction SilentlyContinue
Set-StrictMode -Version 3   

#Login to the Azure Resource Management Account
Login-AzureRmAccount

#Construct Azure Resource Manager parameters
$OptionalParameters = New-Object -TypeName Hashtable
$DateFormat = Get-Date -Format "yyyyMMdd-HHmm"
$FileName = "package-" + $dateFormat + ".zip"
$TemplateFile = [System.IO.Path]::Combine($PSScriptRoot, $TemplateFile)
$TemplateParametersFile = [System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile)

#Construct deployment parameters
$DeployStorageAccountName = $ResourceGroupName.ToLowerInvariant() + 'deployment'
$DeployStorageAccountName = $DeployStorageAccountName -replace '-'


#Create Azure Resource Group
Write-Host "Creating Azure Resource Group: " $ResourceGroupName " in " $ResourceGroupLocation -ForegroundColor Green
New-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Force -ErrorAction Stop 

#Create Azure Storage Account for the deployment of the resources
Write-Host "Creating Azure Storage Account in Resource Group : " $ResourceGroupName " named " $DeployStorageAccountName -ForegroundColor Green
New-AzureRmStorageAccount -ResourceGroupName $ResourceGroupName -Name $DeployStorageAccountName -Type "Standard_LRS" -Location $ResourceGroupLocation

#Create storage container for the deployment files
Write-Host "Creating storage container for the deployment files named: "  $DeployContainer -ForegroundColor Green
$StorageAccountContext = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $DeployStorageAccountName }).Context
$StorageContainer = Get-AzureStorageContainer -Name $DeployContainer -Context $StorageAccountContext -ErrorAction SilentlyContinue
if($StorageContainer -eq $null){
    New-AzureStorageContainer -Name $DeployContainer -Context $StorageAccountContext
}

#Adding the package to the container
Write-Host "Adding the deployment package to the container with the name: " $FileName -ForegroundColor Green
Set-AzureStorageBlobContent -File $WebApiPackage -Blob $FileName -Container 'deployment' -Context $StorageAccountContext -Force

#Setting the optional parameters for the deployment file
$ArtifactsLocation = $StorageAccountContext.BlobEndPoint + $DeployContainer
$ArtifactsLocationSasToken = New-AzureStorageContainerSASToken -Container $DeployContainer -Context $StorageAccountContext -Permission r -ExpiryTime (Get-Date).AddHours(4)
$ArtifactsLocationSasToken = ConvertTo-SecureString $ArtifactsLocationSasToken -AsPlainText -Force

$OptionalParameters = New-Object -TypeName Hashtable
$OptionalParameters['_artifactsLocation'] = $ArtifactsLocation
$OptionalParameters['_artifactsLocationSasToken'] = $ArtifactsLocationSasToken
$OptionalParameters['_packageFileName'] = $FileName

#Deploying resources with the resource management file
Write-Host "Deploying resources to the Resource Group: " $ResourceGroupName -ForegroundColor Green
New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) `
                                   -ResourceGroupName $ResourceGroupName `
                                   -TemplateFile $TemplateFile `
                                   -TemplateParameterFile $TemplateParametersFile `
                                   @OptionalParameters `
                                   -Force

Parameters File

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
  "parameters": {
    "hostingPlanName": { "value": "[Service Plan Name]" },
    "skuName": { "value": "[Resource Level]" },
    "skuCapacity": { "value": [Capacity] },
    "resourceLocation": { "value": "[Resource Location]" },
    "storageName": { "value": "[Storage Name]" },
    "storageType": { "value": "[Storage Type]" },
    "apiName": { "value": "[API Application Name]" },
    "insightsName": { "value": "[Insights Name]" },
  }
}

Azure Resource Template

{
  "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "hostingPlanName": {
      "type": "string",
      "minLength": 1
    },
    "resourceLocation": {
      "type": "string",
      "defaultValue": "West Europe"
    },
    "skuName": {
      "type": "string",
      "defaultValue": "F1",
      "allowedValues": [ "F1", "D1", "B1", "B2", "B3", "S1", "S2", "S3", "P1", "P2", "P3", "P4" ]
    },
    "skuCapacity": {
      "type": "int",
      "defaultValue": 1,
      "minValue": 1
    },
    "storageType": {
      "type": "string",
      "defaultValue": "Standard_LRS",
      "allowedValues": [ "Standard_LRS", "Standard_ZRS", "Standard_GRS", "Standard_RAGRS", "Premium_LRS" ]
    },
    "storageName": {
      "type": "string",
      "minLength": 1
    },
    "apiName": {
      "type": "string",
      "minLength": 1
    },
    "insightsName": {
      "type": "string",
      "minLength": 1
    },
    "_artifactsLocation": {
      "type": "string"
    },
    "_artifactsLocationSasToken": {
      "type": "securestring"
    },
    "_packageFileName": {
      "type": "string"
    }
  },
  "variables": {
    "storageAccountName": "[parameters('storageName')]",
    "storageid": "[resourceId('Microsoft.Storage/storageAccounts',variables('storageAccountName'))]"
  },
  "resources": [
    {
      "apiVersion": "2015-08-01",
      "name": "[parameters('hostingPlanName')]",
      "type": "Microsoft.Web/serverfarms",
      "location": "[parameters('resourceLocation')]",
      "tags": {
        "displayName": "HostingPlan"
      },
      "sku": {
        "name": "[parameters('skuName')]",
        "capacity": "[parameters('skuCapacity')]"
      },
      "properties": {
        "name": "[parameters('hostingPlanName')]"
      }
    },
    {
      "apiVersion": "2015-08-01",
      "name": "[parameters('apiName')]",
      "kind" :  "api",
      "type": "Microsoft.Web/sites",
      "location": "[resourceGroup().location]",
      "tags": {
        "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource",
        "displayName": "Website"
      },
      "dependsOn": [
        "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
      ],
      "properties": {
        "name": "[parameters('apiName')]",
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]",
        "clientCertEnabled": true,
        "siteConfig": {
          "AlwaysOn" :  true
        }
      },
      "resources": [
        {
          "name": "appsettings",
          "type": "config",
          "apiVersion": "2015-08-01",
          "dependsOn": [
            "[concat('Microsoft.Web/sites/', parameters('apiName'))]",
            "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
          ],
          "tags": {
            "displayName": "AppSettings"
          },
          "properties": {
            "StorageConnectionString": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(variables('storageAccountName'),'2016-01-01').keys[0].value)]",
            "InstrumentationKey": "[reference(concat('Microsoft.Insights/components/', parameters('insightsName'))).InstrumentationKey]"
          }
        },
          {
              "name": "MSDeploy",
              "type": "extensions",
              "location": "[resourceGroup().location]",
              "apiVersion": "2015-08-01",
              "dependsOn": [
                  "[concat('Microsoft.Web/sites/', parameters('apiName'))]"
              ],
              "tags": {
                  "displayName": "Deploy"
              },
            "properties": {
              "packageUri": "[concat(parameters('_artifactsLocation'), '/', parameters('_packageFileName'), parameters('_artifactsLocationSasToken'))]",
              "dbType": "None",
              "connectionString": "",
              "setParameters": {
                "IIS Web Application Name": "[parameters('apiName')]"
              }
            }
          }
      ]
    },
    {
      "apiVersion": "2014-04-01",
      "name": "[concat(parameters('hostingPlanName'), '-', resourceGroup().name)]",
      "type": "Microsoft.Insights/autoscalesettings",
      "location": "[resourceGroup().location]",
      "tags": {
        "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource",
        "displayName": "AutoScaleSettings"
      },
      "dependsOn": [
        "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
      ],
      "properties": {
        "profiles": [
          {
            "name": "Default",
            "capacity": {
              "minimum": 1,
              "maximum": 2,
              "default": 1
            },
            "rules": [
              {
                "metricTrigger": {
                  "metricName": "CpuPercentage",
                  "metricResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]",
                  "timeGrain": "PT1M",
                  "statistic": "Average",
                  "timeWindow": "PT10M",
                  "timeAggregation": "Average",
                  "operator": "GreaterThan",
                  "threshold": 80.0
                },
                "scaleAction": {
                  "direction": "Increase",
                  "type": "ChangeCount",
                  "value": 1,
                  "cooldown": "PT10M"
                }
              },
              {
                "metricTrigger": {
                  "metricName": "CpuPercentage",
                  "metricResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]",
                  "timeGrain": "PT1M",
                  "statistic": "Average",
                  "timeWindow": "PT1H",
                  "timeAggregation": "Average",
                  "operator": "LessThan",
                  "threshold": 60.0
                },
                "scaleAction": {
                  "direction": "Decrease",
                  "type": "ChangeCount",
                  "value": 1,
                  "cooldown": "PT1H"
                }
              }
            ]
          }
        ],
        "enabled": false,
        "name": "[concat(parameters('hostingPlanName'), '-', resourceGroup().name)]",
        "targetResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
      }
    },
    {
      "apiVersion": "2014-04-01",
      "name": "[concat('ServerErrors ', parameters('apiName'))]",
      "type": "Microsoft.Insights/alertrules",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Web/sites/', parameters('apiName'))]"
      ],
      "tags": {
        "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('apiName'))]": "Resource",
        "displayName": "ServerErrorsAlertRule"
      },
      "properties": {
        "name": "[concat('ServerErrors ', parameters('apiName'))]",
        "description": "[concat(parameters('apiName'), ' has some server errors, status code 5xx.')]",
        "isEnabled": false,
        "condition": {
          "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition",
          "dataSource": {
            "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource",
            "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('apiName'))]",
            "metricName": "Http5xx"
          },
          "operator": "GreaterThan",
          "threshold": 0.0,
          "windowSize": "PT5M"
        },
        "action": {
          "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction",
          "sendToServiceOwners": true,
          "customEmails": [ ]
        }
      }
    },
    {
      "apiVersion": "2014-04-01",
      "name": "[concat('ForbiddenRequests ', parameters('apiName'))]",
      "type": "Microsoft.Insights/alertrules",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Web/sites/', parameters('apiName'))]"
      ],
      "tags": {
        "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('apiName'))]": "Resource",
        "displayName": "ForbiddenRequestsAlertRule"
      },
      "properties": {
        "name": "[concat('ForbiddenRequests ', parameters('apiName'))]",
        "description": "[concat(parameters('apiName'), ' has some requests that are forbidden, status code 403.')]",
        "isEnabled": false,
        "condition": {
          "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition",
          "dataSource": {
            "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource",
            "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('apiName'))]",
            "metricName": "Http403"
          },
          "operator": "GreaterThan",
          "threshold": 0,
          "windowSize": "PT5M"
        },
        "action": {
          "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction",
          "sendToServiceOwners": true,
          "customEmails": [ ]
        }
      }
    },
    {
      "apiVersion": "2014-04-01",
      "name": "[concat('CPUHigh ', parameters('hostingPlanName'))]",
      "type": "Microsoft.Insights/alertrules",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
      ],
      "tags": {
        "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource",
        "displayName": "CPUHighAlertRule"
      },
      "properties": {
        "name": "[concat('CPUHigh ', parameters('hostingPlanName'))]",
        "description": "[concat('The average CPU is high across all the instances of ', parameters('hostingPlanName'))]",
        "isEnabled": false,
        "condition": {
          "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition",
          "dataSource": {
            "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource",
            "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]",
            "metricName": "CpuPercentage"
          },
          "operator": "GreaterThan",
          "threshold": 90,
          "windowSize": "PT15M"
        },
        "action": {
          "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction",
          "sendToServiceOwners": true,
          "customEmails": [ ]
        }
      }
    },
    {
      "apiVersion": "2014-04-01",
      "name": "[concat('LongHttpQueue ', parameters('hostingPlanName'))]",
      "type": "Microsoft.Insights/alertrules",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
      ],
      "tags": {
        "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource",
        "displayName": "LongHttpQueueAlertRule"
      },
      "properties": {
        "name": "[concat('LongHttpQueue ', parameters('hostingPlanName'))]",
        "description": "[concat('The HTTP queue for the instances of ', parameters('hostingPlanName'), ' has a large number of pending requests.')]",
        "isEnabled": false,
        "condition": {
          "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition",
          "dataSource": {
            "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource",
            "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]",
            "metricName": "HttpQueueLength"
          },
          "operator": "GreaterThan",
          "threshold": 100.0,
          "windowSize": "PT5M"
        },
        "action": {
          "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction",
          "sendToServiceOwners": true,
          "customEmails": [ ]
        }
      }
    },
    {
      "apiVersion": "2014-04-01",
      "name": "[parameters('insightsName')]",
      "type": "Microsoft.Insights/components",
      "location": "Central US",
      "dependsOn": [
        "[concat('Microsoft.Web/sites/', parameters('apiName'))]"
      ],
      "tags": {
        "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('apiName'))]": "Resource",
        "displayName": "AppInsightsComponent"
      },
      "properties": {
        "applicationId": "[parameters('apiName')]"
      }
    },
    {
      "name": "[variables('storageAccountName')]",
      "type": "Microsoft.Storage/storageAccounts",