DevOps Basics: Infrastructure as Code – ARM Templates
Hello Folks,
Any solution you deploy in the cloud requires some kind of supporting infrastructure. (Such as a virtual network, database server, database, website…) The traditional way of managing\deploying these was to build each part one by one.
- Step 1 – Create the storage account
- Step 2 – Create the cloud service
- Step 3 – Create the virtual network
- ….
- Step N - ….. And so on….
This is a complicated way to deploy solutions. Especially if you need to duplicate the infrastructure in Dev, QA, UAT, pre-Prod and Prod for example. By automating and scripting the deployment based on a template you ensure that your solution will always meet your requirements.
As we have been demonstrating in the last 3 posts, we can now using different tools and methods deploy our environments using templates that ensure that our environments are ALWAYS the same in a controlled, repeatable, transferable and efficient manner.
- DevOps Basics: Infrastructure as Code
- DevOps Basics: Infrastructure as Code – The PowerShell Method
- DevOps Basics: Infrastructure as Code – The Visual Studio Method
And so far we have not really looked into the template themselves. In the past 3 post we deployed the exact same template using the 3 methods (portal, PowerShell, Visual Studio). So today we look at templates. More specifically its structure and composition.
Opening the Templates
As I mentioned in the “DevOps Basics: Infrastructure as Code – The PowerShell Method” post. You can use templates from the Gallery, or other sources. Since we want to look at the template we have been using we need to figure out where it is.
To do that, go back to the Azure Quickstart Templates in Azure and select the template we previously selected. On the third page, click on the first template. (Deploy a simple Windows VM in West US)
Once on that page, scroll down to see the Use the template, PowerShell section. There you will see the location on the template. We know it’s in GitHub, and we know its name. (It’s in the URL) https://raw.githubusercontent.com/azure/azure-quickstart-templates/master/101-simple-windows-vm/azuredeploy.json
To look at the code itself you can use any editor (Visual Studio, Notepad, Notepad ++,…) I decided to use Visual Studio Code. It’s free and it works cross platform (Linux, Mac OSX, and Windows)
Once installed I start Code and select File (1) and Open Folder (2)
Once the Select Folder dialogue box opens, browse to the location of the cloned GitHub repository (1) (see DevOps Basics: Infrastructure as Code – The Visual Studio Method for steps to clone the Github repository), select the Azure-quickstart-templates folder (2) , and click Select Folder (3) .
You will now see all the templates you cloned from the Git repository.
Let’s scroll down to “101-simple-windows-VM” as we identified it earlier. You’ll be able to access, open and edit the JSON files needed for the script. (JSON (JavaScript Object Notation) is a lightweight data-interchange format. It is easy for humans to read and write. It is easy for machines to parse and generate) https://json.org/
You’ll notice that the basic structure of a template is as follows:
{
"$schema": https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#,
"contentVersion": "",
"parameters": { },
"variables": { },
"resources": ],
"outputs": { }
}
Element name |
Required |
Description |
$schema |
Yes |
Location of the JSON schema file that describes the version of the template language. |
contentVersion |
Yes |
Version of the template (such as 1.0.0.0). When deploying resources using the template, this value can be used to make sure that the right template is being used. |
parameters |
No |
Values that are provided when deployment is executed to customize resource deployment. |
variables |
No |
Values that are used as JSON fragments in the template to simplify template language expressions. |
resources |
Yes |
Types of services that are deployed or updated in a resource group. |
outputs |
No |
Values that are returned after deployment. |
$schema
In our template the $schema and the contentVersion are the default ones.
"$schema": "https://schema.management.azure.com/schemas/ 2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
Each parameters outline values needed to customize our deployment. In our case newStorageAccountName, adminUsername, adminPassword, dnsNameForPublicIP, windowsOSVersion are the values a user can input when deploying the resources. You can use these parameter values throughout the template to set values for the deployed resources. Only parameters that are declared in the parameters section can be used in other sections of the template. Within parameters section, you cannot use a parameter value to construct another parameter value. That type of operation typically happens in the variables section.
Element name |
Required |
Description |
parameterName |
Yes |
Name of the parameter. Must be a valid JavaScript identifier. |
type |
Yes |
Type of the parameter value. See the list below of allowed types. The allowed types and values are:
(All passwords, keys, and other secrets should use the secureString type. Template parameters with the secureString type cannot be read after resource deployment) |
defaultValue |
No |
Default value for the parameter, if no value is provided for the parameter. |
allowedValues |
No |
Array of allowed values for the parameter to make sure that the right value is provided. |
Parameters
In our template the parameters section is populated as: follows.
"parameters": {
"newStorageAccountName": {
"type": "string",
"metadata": {
"description": "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed."
}
},
"adminUsername": {
"type": "string",
"metadata": {
"description": "Username for the Virtual Machine."
}
},
"adminPassword": {
"type": "securestring",
"metadata": {
"description": "Password for the Virtual Machine."
}
},
"dnsNameForPublicIP": {
"type": "string",
"metadata": {
"description": "Unique DNS Name for the Public IP used to access the Virtual Machine."
}
},
"windowsOSVersion": {
"type": "string",
"defaultValue": "2012-R2-Datacenter",
"allowedValues": [
"2008-R2-SP1",
"2012-Datacenter",
"2012-R2-Datacenter"
],
"metadata": {
"description": "The Windows version for the VM. This will pick a fully patched image of this given Windows version. Allowed values:2008-R2-SP1, 2012-Datacenter, 2012-R2-Datacenter."
}
}
},
Variables
In the variables section, you construct values that can be used to simplify template language expressions. Typically, these variables will be based on values provided from the parameters. Or variables that you set. One of the difference between these variables and the parameters mentioned earlier, is that these cannot be changed during the deployment.
In our template the variables section is populated as: follows.
"variables": {
"location": "West US",
"imagePublisher": "MicrosoftWindowsServer",
"imageOffer": "WindowsServer",
"OSDiskName": "osdiskforwindowssimple",
"nicName": "myVMNic",
"addressPrefix": "10.0.0.0/16",
"subnetName": "Subnet",
"subnetPrefix": "10.0.0.0/24",
"storageAccountType": "Standard_LRS",
"publicIPAddressName": "myPublicIP",
"publicIPAddressType": "Dynamic",
"vmStorageAccountContainerName": "vhds",
"vmName": "MyWindowsVM",
"vmSize": "Standard_A2",
"virtualNetworkName": "MyVNET",
"vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]",
"subnetRef": "[concat(variables('vnetID'),'/subnets/', variables('subnetName'))]"
},
You can notice that the last 2 variables are constructed based on existing variables and parameters.
"vnetID": "[resourceId('Microsoft.Network/virtualNetworks' variables('virtualNetworkName'))]",
and
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]"
You can find out more about all the functions that can be used to construct variables here.
Resources
In the resources section, you define the resources are deployed or updated.
There is a specific structure that you need to take care of when you define a resource.
"resources": [
{
"apiVersion": "<api-version-of-resource>",
"type": "<resource-provider-namespace/resource-type-name>",
"name": "<name-of-the-resource>",
"location": "<location-of-resource>",
"tags": "<name-value-pairs-for-resource-tagging>",
"dependsOn": [
"<array-of-related-resource-names>"
],
"properties": "<settings-for-the-resource>",
"resources": [
"<array-of-dependent-resources>"
]
}
]
Each elements of the structure are defined below.
Element name |
Required |
Description |
apiVersion |
Yes |
Version of the API that supports the resource. |
type |
Yes |
Type of the resource. This value is a combination of the namespace of the resource provider and the resource type that the resource provider supports |
name |
Yes |
Name of the resource. The name must follow URI component restrictions defined in RFC3986. |
location |
No |
Supported geo-locations of the provided resource. |
tags |
No |
Tags that are associated with the resource. |
dependsOn |
No |
Resources that the resource being defined depends on. The dependencies between resources are evaluated and resources are deployed in their dependent order. When resources are not dependent on each other, they are attempted to be deployed in parallel. The value can be a comma separated list of a resource names or resource unique identifiers. |
properties |
No |
Resource specific configuration settings. |
resources |
No |
Child resources that depend on the resource being defined. |
To find out all the types available, use PowerShell. The following command
Get-AzureProvider –ListAvailable
For example, if you to define a virtual machine in a template the list above lists all the Microsoft.Compute types and in the list you can use Microsoft.Compute\virtualmachines as it is defined in our own template. (See the partial code below)
{
"apiVersion": "2015-05-01-preview",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[variables('location')]",
"dependsOn": [
"[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]",
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[variables('vmSize')]"
},
"osProfile": {
"computername": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku" : "[parameters('windowsOSVersion')]",
"version":"latest"
},
"osDisk" : {
"name": "osdisk",
"vhd": {
"uri": "[concat('https://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('OSDiskName'),'.vhd')]"
},
"caching": "ReadWrite",
"createOption": "FromImage"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
}
]
}
}
}
Here is the complete resource section of our template.
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"name": "[parameters('newStorageAccountName')]",
"apiVersion": "2015-05-01-preview",
"location": "[variables('location')]",
"properties": {
"accountType": "[variables('storageAccountType')]"
}
},
{
"apiVersion": "2015-05-01-preview",
"type": "Microsoft.Network/publicIPAddresses",
"name": "[variables('publicIPAddressName')]",
"location": "[variables('location')]",
"properties": {
"publicIPAllocationMethod": "[variables('publicIPAddressType')]",
"dnsSettings": {
"domainNameLabel": "[parameters('dnsNameForPublicIP')]"
}
}
},
{
"apiVersion": "2015-05-01-preview",
"type": "Microsoft.Network/virtualNetworks",
"name": "[variables('virtualNetworkName')]",
"location": "[variables('location')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"[variables('addressPrefix')]"
]
},
"subnets": [
{
"name": "[variables('subnetName')]",
"properties": {
"addressPrefix": "[variables('subnetPrefix')]"
}
}
]
}
},
{
"apiVersion": "2015-05-01-preview",
"type": "Microsoft.Network/networkInterfaces",
"name": "[variables('nicName')]",
"location": "[variables('location')]",
"dependsOn": [
"[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
"[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
],
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]"
},
"subnet": {
"id": "[variables('subnetRef')]"
}
}
}
]
}
},
{
"apiVersion": "2015-05-01-preview",
"type": "Microsoft.Compute/virtualMachines",
"name": "[variables('vmName')]",
"location": "[variables('location')]",
"dependsOn": [
"[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]",
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "[variables('vmSize')]"
},
"osProfile": {
"computername": "[variables('vmName')]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"imageReference": {
"publisher": "[variables('imagePublisher')]",
"offer": "[variables('imageOffer')]",
"sku" : "[parameters('windowsOSVersion')]",
"version":"latest"
},
"osDisk" : {
"name": "osdisk",
"vhd": {
"uri": "[concat('https://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('OSDiskName'),'.vhd')]"
},
"caching": "ReadWrite",
"createOption": "FromImage"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
}
]
}
}
}
]
}
There you go. A look throughout the 101-simple-windows-vm template.
I really encourage you to play with this. you wont be sorry, and you could end up saving yourself some serious time and headaches .
Cheers!
Pierre Roman
@pierreroman