In my last post I discussed the basics of ARM templates and the Azure Resource Manager. In the following article, I would like to discuss a few other ways to deploy larger infrastructures, including dependencies, via an ARM template.
The example infrastructure to be built here consists of the following components:
- Web API (App Service)
- Deployment / Staging Slot für die Web API
- SQL Datenbank
- Redis Cache
- Azure Search
- Application Insights
- Auto-Scale Settings
- Alerts
All in all, a typical landscape for the backend of a modern web / mobile application. What is not covered in this example, is the Continuous Integration / Deployment area for the API or the database. The focus is entirely on the infrastructure and the corresponding “wiring” of the components. To illustrate the dependencies, here is a quick chart:
I will not go into all the details of the template, but point out the important sections that e.g. connect the WebApp to Azure Search, create the Content Delivery Network, create the staging slot etc.
Let’s get to the individual components…
WebAPI
The creation of the WebAPI differs marginally from the standard template, which can be generated via Visual Studio. Since there is only one API running in the WebApp and .NET code will be executed, some settings are adapted to it. Among other things, PHP (line 65) and ARR (line 18) are disabled, the AlwaysOn feature enabled (line 64), the 64bit environment activated (line 63), the default page is removed (line 66). In addition, all connection strings (starting with Line 20) and properties , which are necessary for the communication with other services in the template (starting line 38), are filled out during the deployment.
{ | |
"apiVersion": "2015-08-01", | |
"name": "[variables('apiSiteName')]", | |
"type": "Microsoft.Web/sites", | |
"location": "[resourceGroup().location]", | |
"dependsOn": [ | |
"[resourceId('Microsoft.Cache/Redis/', variables('RedisCache').Name)]", | |
"[resourceId('Microsoft.Search/searchServices/', variables('searchAppName'))]", | |
"[resourceId('Microsoft.Cdn/profiles/', variables('profileName'))]", | |
"[resourceId('Microsoft.Web/serverFarms/', parameters('apiHostingPlanName'))]" | |
], | |
"tags": { | |
"displayName": "Web / API" | |
}, | |
"properties": { | |
"name": "[variables('apiSiteName')]", | |
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('apiHostingPlanName'))]", | |
"clientAffinityEnabled": false | |
}, | |
"resources": [{ | |
"apiVersion": "2015-08-01", | |
"type": "config", | |
"name": "connectionstrings", | |
"dependsOn": [ | |
"[resourceId('Microsoft.Web/Sites/', variables('apiSiteName'))]" | |
], | |
"properties": { | |
"masterdata": { | |
"value": "[concat('Data Source=tcp:', reference(resourceId('Microsoft.Sql/servers/', variables('sqlserverName'))).fullyQualifiedDomainName, ',1433;Initial Catalog=', parameters('databaseName'), ';User Id=', parameters('administratorLogin'), '@', variables('sqlserverName'), ';Password=', parameters('administratorLoginPassword'), ';')]", | |
"type": "SQLServer" | |
}, | |
"redis": { | |
"value": "[concat(reference(resourceId('Microsoft.Cache/Redis', variables('RedisCache').Name)).hostName, variables('RedisCache').WebAppRedisSettingText, listKeys(resourceId('Microsoft.Cache/Redis', variables('RedisCache').Name), '2015-08-01').primaryKey)]", | |
"type": "Custom" | |
} | |
} | |
}, | |
{ | |
"apiVersion": "2015-08-01", | |
"type": "config", | |
"name": "appsettings", | |
"dependsOn": [ | |
"[resourceId('Microsoft.Web/Sites/', variables('apiSiteName'))]" | |
], | |
"properties": { | |
"Redis:InstanceName": "[variables('cacheInstanceName')]", | |
"ApplicationInsights:InstrumentationKey": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2015-05-01').InstrumentationKey]", | |
"Search:ServiceName": "[variables('searchAppName')]", | |
"Search:Key": "[listAdminKeys(resourceId('Microsoft.Search/searchServices/', variables('searchAppName')), '2015-08-19').primaryKey]", | |
"Storage:AccountName": "[variables('storageName')]", | |
"Storage:AccountKey": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageName')), '2015-06-15').key1]", | |
"CDN:Hostname": "[reference(resourceId('Microsoft.Cdn/profiles/endpoints',variables('profileName'),variables('endpointName')),'2016-04-02').hostName]" | |
} | |
}, | |
{ | |
"apiVersion": "2015-08-01", | |
"type": "config", | |
"name": "web", | |
"dependsOn": [ | |
"[resourceId('Microsoft.Web/Sites/', variables('apiSiteName'))]" | |
], | |
"properties": { | |
"use32BitWorkerProcess": false, | |
"alwaysOn": true, | |
"phpVersion": "", | |
"defaultDocuments": [] | |
} | |
} | |
] | |
} |
Deployment Slot / Staging
A deployment slot provides an additional endpoint / additional WebApp within the app service plan, into which the actual app can be deployed. Deployment slots are a popular feature to launch a new version of an application with almost zero-downtime. You deploy the new version into a slot, test the functionality, generate load to “warm up” the app and then swap the production slot with the staging slot (“swapping” – corresponds to an IP switch on the contained loadbalancer). This can be done either via the portal, Powershell or e.g. Visual Studio Team Services.
The template for the “staging slot” to be created is as follows:
{ | |
"name": "[concat(variables('apiSiteName'), '/staging')]", | |
"type": "Microsoft.Web/sites/slots", | |
"location": "[resourceGroup().location]", | |
"apiVersion": "2015-08-01", | |
"dependsOn": [ | |
"[resourceId('Microsoft.Web/Sites', variables('apiSiteName'))]" | |
], | |
"tags": { | |
"[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('apiHostingPlanName'))]": "Resource", | |
"displayName": "Slot Web / API" | |
}, | |
"properties": { | |
"name": "[concat(variables('apiSiteName'), '(staging)')]", | |
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', parameters('apiHostingPlanName'))]", | |
"clientAffinityEnabled": false | |
}, | |
"resources": [{ | |
"apiVersion": "2015-08-01", | |
"name": "connectionstrings", | |
"type": "config", | |
"location": "[resourceGroup().location]", | |
"dependsOn": [ | |
"[concat('Microsoft.Web/Sites/', variables('apiSiteName'), '/slots/staging')]" | |
], | |
"properties": { | |
"masterdata": { | |
"value": "[concat('Data Source=tcp:', reference(resourceId('Microsoft.Sql/servers/', variables('sqlserverName'))).fullyQualifiedDomainName, ',1433;Initial Catalog=', parameters('databaseName'), ';User Id=', parameters('administratorLogin'), '@', variables('sqlserverName'), ';Password=', parameters('administratorLoginPassword'), ';')]", | |
"type": "SQLServer" | |
}, | |
"redis": { | |
"value": "[concat(reference(resourceId('Microsoft.Cache/Redis', variables('RedisCache').Name)).hostName, variables('RedisCache').WebAppRedisSettingText, listKeys(resourceId('Microsoft.Cache/Redis', variables('RedisCache').Name), '2015-08-01').primaryKey)]", | |
"type": "Custom" | |
} | |
} | |
}, | |
{ | |
"apiVersion": "2015-08-01", | |
"type": "config", | |
"name": "web", | |
"dependsOn": [ | |
"[concat('Microsoft.Web/Sites/', variables('apiSiteName'), '/slots/staging')]" | |
], | |
"properties": { | |
"use32BitWorkerProcess": false, | |
"alwaysOn": true, | |
"phpVersion": "", | |
"defaultDocuments": [] | |
} | |
} | |
] | |
} |
Storage Account and Content Delivery Network
Nearly every web application / API must be able to deal with files and the associated “delivery”. A storage account (Blob Storage) is created for the storage of files. A content delivery network is then created, which has the appropriate storage account as the source. The important thing is that the container, which is responsible for storing the files in the storage account, is created at least with the “Blobs (anonymous read access for blobs only)” permission. Unfortunately, you can not create containers by ARM template at the time of writing (see: Let me define preconfigured Blob Containers, Tables, Queue in ARM template).
Important in this section of the template is the “linking” of the CDN with the storage account (starting from Line 31).
{ | |
"name": "[variables('storageName')]", | |
"type": "Microsoft.Storage/storageAccounts", | |
"location": "[resourceGroup().location]", | |
"apiVersion": "2016-01-01", | |
"sku": { | |
"name": "Standard_LRS", | |
"tier": "Standard" | |
}, | |
"kind": "BlobStorage", | |
"dependsOn": [], | |
"tags": { | |
"displayName": "Storage Blobs / CDN backend" | |
}, | |
"properties": { | |
"accessTier": "Hot" | |
} | |
}, | |
{ | |
"name": "[variables('profileName')]", | |
"type": "Microsoft.Cdn/profiles", | |
"location": "[resourceGroup().location]", | |
"apiVersion": "2016-04-02", | |
"tags": { | |
"displayName": "CDN" | |
}, | |
"sku": { | |
"name": "Standard_Akamai" | |
}, | |
"properties": {}, | |
"resources": [{ | |
"apiVersion": "2016-04-02", | |
"name": "[variables('endpointName')]", | |
"type": "endpoints", | |
"dependsOn": [ | |
"[resourceId('Microsoft.Cdn/profiles/', variables('profileName'))]", | |
"[resourceId('Microsoft.Storage/storageAccounts/', variables('storageName'))]" | |
], | |
"location": "[resourceGroup().location]", | |
"tags": { | |
"displayName": "CDN Endpoint" | |
}, | |
"properties": { | |
"originHostHeader": "[replace(replace(reference(resourceId('Microsoft.Storage/storageAccounts',variables('storageName')),'2015-06-15').primaryEndpoints.blob,'https://',''),'/','')]", | |
"isHttpAllowed": true, | |
"isHttpsAllowed": true, | |
"queryStringCachingBehavior": "IgnoreQueryString", | |
"contentTypesToCompress": ["text/plain", "text/html", "text/css", "application/x-javascript", "text/javascript"], | |
"isCompressionEnabled": true, | |
"origins": [{ | |
"name": "origin1", | |
"properties": { | |
"hostName": "[replace(replace(reference(resourceId('Microsoft.Storage/storageAccounts',variables('storageName')),'2015-06-15').primaryEndpoints.blob,'https://',''),'/','')]" | |
} | |
}] | |
} | |
}] | |
} |
Azure Redis Cache and Azure Search
In order to provide users with the best possible performance of an application, caches (for general requirements / caching) or search engines (especially for the search in the respective data) are often used. Azure has the Azure Redis Cache and Azure Search offers in the service portfolio. The creation of the services is relatively simple, as you can see in the following template section.
{ | |
"type": "Microsoft.Cache/Redis", | |
"name": "[variables('RedisCache').Name]", | |
"apiVersion": "2016-04-01", | |
"location": "[resourceGroup().location]", | |
"tags": { | |
"displayName": "Redis Cache" | |
}, | |
"properties": { | |
"redisVersion": "3.0", | |
"sku": { | |
"name": "[variables('RedisCache').SKUName]", | |
"family": "[variables('RedisCache').SKUFamily]", | |
"capacity": "[variables('RedisCache').SKUCapacity]" | |
}, | |
"enableNonSslPort": false, | |
"redisConfiguration": { | |
"maxclients": "256" | |
} | |
}, | |
"resources": [], | |
"dependsOn": [] | |
}, | |
{ | |
"apiVersion": "2015-08-19", | |
"name": "[variables('searchAppName')]", | |
"type": "Microsoft.Search/searchServices", | |
"location": "[resourceGroup().location]", | |
"properties": { | |
"replicaCount": "1", | |
"partitionCount": "1", | |
"hostingMode": "default" | |
}, | |
"sku": { | |
"name": "basic" | |
}, | |
"tags": { | |
"displayName": "Search Service" | |
} | |
} |
The interesting thing about the two created resources is the selection of the admin keys, so that the web app gets access to the respective service. Similar to the connection string of the SQL server, connections strings / properties must be created with which the connection from the Web app can be established. The access to the keys of the Redis Cache and Azure Search are located in the web app resource (lines 33 and 49), for completeness here again:
// Redis | |
"redis": { | |
"value": "[concat(reference(resourceId('Microsoft.Cache/Redis', variables('RedisCache').Name)).hostName, variables('RedisCache').WebAppRedisSettingText, listKeys(resourceId('Microsoft.Cache/Redis', variables('RedisCache').Name), '2015-08-01').primaryKey)]", | |
"type": "Custom" | |
} | |
// Search | |
"properties": { | |
"Search:Key": "[listAdminKeys(resourceId('Microsoft.Search/searchServices/', variables('searchAppName')), '2015-08-19').primaryKey]", | |
} |
The two functions listKeys and listAdminKeys are so-called resource functions, which are made available by the underlying resource provider. Theoretically, each resource provider can provide such functions. To find out which functions a provider supports, there is the possibility to “ask” via Powershell (or CLI, REST api) at the desired provider. For Azure Storage, Redis or Azure Search, the calls would look like this:
Get-AzureRmProviderOperation -OperationSearchString "Microsoft.Storage/*" | where {$_.Operation -like "*list*"} | FT Operation Get-AzureRmProviderOperation -OperationSearchString "Microsoft.Cache/*" | where {$_.Operation -like "*list*"} | FT Operation Get-AzureRmProviderOperation -OperationSearchString "Microsoft.Search/*" | where {$_.Operation -like "*list*"} | FT Operation
The official documentation for the template functions, in particular resource functions, can be found here: Resource functions for Azure Resource Manager templates.
Other Resources
The template used here contains other resources, such as application insights, scale settings and alerts for the web application, SQL DB etc., which are created “straightforward” and for which no further explanations are necessary.
In the end, settings / information, such as the instrumentation key of AppInsights or the connection string of the database, are entered automatically during deployment time in the corresponding resource (mainly the WebApp).
Deployment
To deploy the template, there are several options available, which I have already briefly discussed in the previous article. Therefore, I only provide the Powershell script here:
# Login to Azure Login-AzureRmAccount # Resource Group $rg = "myappinfra-rg" New-AzureRmResourceGroup -Name $rg -Location westeurope # Deploy via Resource Group Deployment New-AzureRmResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile ".\azuredeploy.json" -TemplateParameterFile ".\azuredeploy.parameters.json"
Wrap Up
The template including parameter file and deployment script are available on GitHub.
Have fun with 🙂