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.

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)

image

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) .

image

You will now see all the templates you cloned from the Git repository.

image

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/

image

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:

  • string or secureString - any valid JSON string
  • int - any valid JSON integer
  • bool - any valid JSON Boolean
  • object - any valid JSON object
  • array - any valid JSON array

(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

image

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!

Signature

Pierre Roman
@pierreroman