Known configurations, on the other hand, are specific pre-determined configurations for deploying an environment using ARM templates. These pre-determined configurations are known as T-shirt sizing configuration. The T-shirt sizing acts as an analogy. Similar to the way a T-shirt is available in a pre-determined configuration such as small, medium, and large, ARM templates can be pre-configured to deploy a small, medium, or large environment depending on the need. This means users cannot determine any custom size for the environment, but they can choose from various options, and ARM templates execution during runtime will ensure that an appropriate configuration of the environment is provisioned.
So the first step in creating a modular ARM template is deciding on the known configurations for an environment.
As an example, here is the configuration of a data center deployment on Azure:
T-shirt size |
ARM template configuration |
Small |
4 virtual machines with 7 GB of memory along with 4 CPU cores |
Medium |
8 virtual machines with 14 GB of memory along with 8 CPU cores |
Large |
16 virtual machines with 28 GB of memory along with 8 CPU cores |
Now that we know the configurations, we can create modular ARM templates.
There are two ways to write modular ARM templates:
- Composed templates: Composed templates link to other templates. Examples of composed templates are master and intermediate templates.
- Leaf-level templates: Leaf-level templates are templates that contains a single Azure resource.
ARM templates can be divided into modular templates based on the following:
- Technology
- Functionality
An ideal way to decide on the modular method to author an ARM template is as follows:
- Define resource or leaf-level templates consisting of single resources. In the upcoming diagram, the extreme right templates are leaf-level templates. Within the diagram, virtual machines, virtual network, storage and others in the same column represent leaf-level templates.
- Compose environment specific templates using the leaf-level templates. These environment specific templates provisions an Azure environment such as an SQL Server environment, an app service environment, or a data center environment. Let's drill down a bit more into this topic. Let's take the example of an Azure SQL environment. To create an Azure SQL environment, multiple resources are needed. At a bare minimum, a logical SQL Server, an SQL database, and a few SQL firewall resources should be provisioned. All these resources are defined in individual templates at leaf level. These resources can be composed together in a single template that has capability to create an Azure SQL environment. Anybody wanting to create an SQL environment can use this composed template. The image shown next has Data center, messaging and App Services as environment specific templates.
- Create templates with higher abstraction composing multiple environment specific templates into solutions. These templates are composed of environment specific templates that were created in previous step. For example, to create an e-commerce inventory solution that needs an app service environment and a SQL environment, two environment templates -APP Service environment and an SQL Server environment template can be composed together. The image shown next has functional 1 and functional 2 as solution specific templates.
- Finally, a master template should be created, which should be composed of multiple templates where each template is capable of deploying a solution.
The preceding steps for creating a modular designed template can be easily be understood by means of an diagram, as shown next:
Now, let's implement a part of the functionality shown in the previous diagram. In this implementation, we will provision a virtual machine with a script extension using a modular approach. The custom script extension deploys Docker binaries and prepares a container environment on Windows Server 2016 virtual machine.
Now, we are going to create a solution using ARM templates using a modular approach. As mentioned before, the first step is to create individual resource templates. These individual resource templates will be used to compose additional templates capable of creating an environment. These templates would be needed to create a virtual machine. All arm templates shown here are available in accompanied chapter code. The names and code of these templates are shown next:
- Storage.json
- virtualNetwork.json
- PublicIPAddress.json
- NIC.json
- VirtualMachine.json
- CustomScriptExtension.json
First, let's look at the code for the Storage.json template. This template provisions a storage account that every virtual machine needs for storing its OS and data disk files:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-
01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageAccountName": {
"type": "string",
"minLength": 1
},
"storageType": {
"type": "string",
"minLength": 1
},
...
"outputs": {
"resourceDetails": {
"type": "object",
"value": "[reference(parameters('storageAccountName'))]"
}
}
}
Next, let's look at the code for the public IP address template. A virtual machine that should be accessible over the internet needs a public IP address resource assigned to its network interface card. Although exposing a virtual machine to the internet is optional, this resource optionally might get used for creating a virtual machine. The code shown next is available in PublicIPAddress.json file:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-
01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"publicIPAddressName": {
"type": "string",
"minLength": 1
},
"publicIPAddressType": {
"type": "string",
"minLength": 1
...
}
}
],
"outputs": {
"resourceDetails": {
"type": "object",
"value": "[reference(parameters('publicIPAddressName'))]"
}
}
}
Next, let's look at the code for the virtual network. Virtual machines on Azure need a virtual network for communication. This template will be used to create a virtual network on Azure with pre-defined address range and subnets. The code shown next is available in virtualNetwork.json file:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-
01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"virtualNetworkName": {
"type": "string",
"minLength": 1
...
},
"subnetPrefix": {
"type": "string",
"minLength": 1
},
"resourceLocation": {
"type": "string",
"minLength": 1
}
...
"subnets": [
{
"name": "[parameters('subnetName')]",
"properties": {
"addressPrefix": "[parameters('subnetPrefix')]"
}
}
]
}
}
],
"outputs": {
"resourceDetails": {
"type": "object",
"value": "[reference(parameters('virtualNetworkName'))]"
}
}
}
Next, let's look at the code for the Network Interface Card. A virtual network card is needed by virtual machine to connected to a virtual network and to accept and send requests from and to the internet. The code shown next is available in NIC.json file:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-
01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"nicName": {
"type": "string",
"minLength": 1
},
"publicIpReference": {
"type": "string",
"minLength": 1
...
[resourceId(subscription().subscriptionId,resourceGroup().name, 'Microsoft.Network/publicIPAddresses', parameters('publicIpReference'))]",
"vnetRef": "[resourceId(subscription().subscriptionId,resourceGroup().name, 'Microsoft.Network/virtualNetworks', parameters('virtualNetworkReference'))]",
"subnet1Ref": "[concat(variables('vnetRef'),'/subnets/', parameters('subnetReference'))]"
},
...
"id": "[variables('subnet1Ref')]"
}
}
}
]
}
}
],
"outputs": {
"resourceDetails": {
"type": "object",
"value": "[reference(parameters('nicName'))]"
}
}
}
Next, let's look at the code for creating a virtual machine. Each virtual machine is a resource in Azure, and note that this template has no reference to storage, network, public IP address, or other resources created earlier. This reference and composition will happen later in this section using another template. The code shown next is available in VirtualMachine.json file:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01
01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"vmName": {
"type": "string",
"minLength": 1
...
},
"imageOffer": {
"type": "string",
"minLength": 1
},
"windowsOSVersion": {
"type": "string",
"minLength": 1
},
...
"outputs": {
"resourceDetails": {
"type": "object",
"value": "[reference(parameters('vmName'))]"
}
}
}
Next, let's look at the code for creating a custom script extension. This resource executes a PowerShell script on a virtual machine after it is provisioned. This resource provides an opportunity to execute post-provisioning tasks in Azure virtual machines. The code shown next is available in CustomScriptExtension.json file:
{
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"VMName": {
"type": "string",
"defaultValue": "sqldock",
"metadata": {
...
"commandToExecute": "[concat('powershell -ExecutionPolicy Unrestricted -file docker.ps1')]"
},
"protectedSettings": {
}
}
}
],
"outputs": {
}
}
Next, we'll look at the custom script extension PowerShell code that prepares the Docker environment. Please note that a virtual machine reboot might happen while executing the PowerShell script, depending on whether the containers Windows feature is already installed or not. The following script installs the NuGet package, the DockerMsftProvider provider, and Docker executable. docker.ps1 file is available with the accompanied chapter code.
#
# docker.ps1
#
Install-PackageProvider -Name Nuget -Force -ForceBootstrap -Confirm:$false
Install-Module -Name DockerMsftProvider -Repository PSGallery -Force -Confirm:$false -verbose
Install-Package -Name docker -ProviderName DockerMsftProvider -Force -ForceBootstrap -Confirm:$false
All the linked templates shown before should be uploaded to a container within an Azure Blob Storage account. This container can have private access policy applied as we saw in the previous chapter however for this example, we will set the access policy as container. This means these linked templates can be accessed without the need of any SAS token.
Finally, let's focus on writing the master template. Within Master template, all the linked templates are composed together to create a solution—to deploy a virtual machine and execute a script within it. The same approach can be used for creating other solutions like provisioning a data center consisting of multiple inter-connected virtual machines. The code shown next is available in Master.json file.
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageAccountName": {
"type": "string",
"minLength": 1
...
},
"subnetName": {
"type": "string",
"minLength": 1
},
"subnetPrefix": {
"type": "string",
"minLength": 1
...
"windowsOSVersion": {
"type": "string",
"minLength": 1
},
"vhdStorageName": {
"type": "string",
"minLength": 1
},
"vhdStorageContainerName": {
"type": "string",
"minLength": 1
...[concat('https://',parameters('storageAccountName'),'armtfiles.blob.core.windows.net/',variables('containerName'),'/Storage.json')]",
"contentVersion": "1.0.0.0"
},
"parameters": {
"storageAccountName": {
"value": "[parameters('storageAccountName')]"
},
"storageType": {
"value": "[parameters('storageType')]"
},
"resourceLocation": {
"value": "[resourceGroup().location]"
...
"outputs": {
"resourceDetails": {
"type": "object",
"value": "[reference('GetVM').outputs.resourceDetails.value]"
}
}
}
The master templates invoke the external templates and also co-ordinates inter-dependencies among them.
The external templates should be available at a well-known location so that the master template can access and invoke them. In this example, the external templates are stored in Azure Blob Storage container and this information was passed to ARM template by means of parameters.
The external templates in Azure Blob Storage could be access protected by setting up access policies. The command used to deploy the master template is shown next. It might look like a complex command but a majority of values are used as parameters. Readers are advised to change the value of these parameters before running it. The linked templates have been uploaded to a storage account named st02gvwldcxm5suwe within armtemplates container. The resource group should be created if it does not exists. The first command is used to create a new Resource Group in West Europe region:
New-AzureRmResourceGroup -Name "testvmrg" -Location "West Europe" -Verbose
Rest of the parameter values are needed for configuring each resource. The storage account name and dnsNameForPublicIP value should be unique within Azure:
New-AzureRmResourceGroupDeployment -Name "testdeploy1" -ResourceGroupName testvmrg -Mode Incremental -TemplateFile "C:\chapter 05\Master.json" -storageAccountName "st02gvwldcxm5suwe" -storageType "Standard_LRS" -publicIPAddressName "uniipaddname" -publicIPAddressType "Dynamic" -dnsNameForPublicIP "azureforarchitectsbook" -virtualNetworkName vnetwork01 -addressPrefix "10.0.1.0/16" -subnetName "subnet01" -subnetPrefix "10.0.1.0/24" -nicName nic02 -vmSize "Standard_DS1" -adminUsername "sysadmin" -adminPassword $(ConvertTo-SecureString -String sysadmin@123 -AsPlainText -Force) -vhdStorageName oddnewuniqueacc -vhdStorageContainerName vhds -OSDiskName mynewvm -vmName vm10 -windowsOSVersion 2012-R2-Datacenter -imagePublisher MicrosoftWindowsServer -imageOffer WindowsServer -containerName armtemplates -Verbose