Minimum Viable ARM Template

ARM Templates are a great way to deploy resources on Azure, but it can be difficult to know how to modify an ARM template to match a specific scenario. In this series of posts, we will define a “minimum viable ARM template” then incrementally add features over time. This way, we can easily see how to modify a template to make it match a specific scenario.

All templates for this series live in this repo: https://github.com/gatneil/MinimumViableArmTemplate. We will begin by looking at the “minimum-viable-vm” branch: https://github.com/gatneil/MinimumViableArmTemplate/blob/minimum-viable-vm/azuredeploy.json.

First, we see the schema and content version. The schema defines which version of the ARM template language we are using. The content version is actually there for us to keep track of which version of this specific template this is. It is not used by the Azure platform at all:

{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
...

Next, we see a section called “parameters”. These are the parameters the template will accept at deploy time. These parameters will be used in the template. For this template, we accept a parameter for “adminUsername” (a string) and “adminPassword” (a securestring, which is a string that gets hidden at times for security purposes). For more information on parameters, see this document: https://docs.microsoft.com/azure/azure-resource-manager/templates/template-parameters:

"parameters": {
"adminUsername": {
"type": "string"
},
"adminPassword": {
"type": "securestring"
}
},

Next, we have a section called “resources”. This is a list of the actual Azure resources we want to deploy. Each resource requires a “type” (this specifies which resource), “apiVersion” (this specifies which API version we will use to create the resource), “name”, and “location” (which Azure region the resource will live in). For all resources in this example, we use API version 2020-06-01, but we may need to upgrade to a later version to access new properties as they are added over time. For a full list of all properties in each API version for each resource type, see the Azure Rest API Specs: https://github.com/Azure/azure-rest-api-specs. For location, we use “[resourceGroup().location]”. This is an example of an ARM Template function. It returns the location of the resource group the template is deployed in. Of course, we can choose to have our resources in different regions if we want (with some exceptions), but for now it’s simpler to keep them all in the same region as our resource group. For more information on ARM Template functions, see this documentation: https://docs.microsoft.com/azure/azure-resource-manager/templates/template-functions. Finally, we have a “properties” section which holds the properties that are unique to that resource type. In this case, we are creating a virtual network, so we need to tell it which address space to use and which subnets to create. For now, we choose 10.0.0.0/8 as the only address space and 10.0.0.0/16 as the only subnet within that address range. Note that the virtual network address prefixes refer to private IP addresses. We will get to public IP addresses in a moment:

"resources": [
{
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2020-06-01",
"name": "myVnet",
"location": "[resourceGroup().location]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"10.0.0.0/8"
]
},
"subnets": [
{
"name": "mySubnet",
"properties": {
"addressPrefix": "10.0.0.0/16"
}
}
]
}
},
...

Next, we define a public IP address and a network interface. The public IP address does not require any specific properties, so we remove the “properties” section for now. However, when we look at the network interface configuration, we see some new concepts. The first is the “dependsOn” clause. This tells Azure that the network interface object depends on the virtual network and the public IP address, so Azure will wait to create the network interface until these other resources are ready. But how does it know which resources to wait on? Each resource in Azure has a unique “resource ID”, and we use the [resourceId()] ARM template function to get the ID of the virtual network and the public IP address (for more details on this function, see this documentation: https://docs.microsoft.com/azure/azure-resource-manager/templates/template-functions-resource#resourceid). Also, we notice that in the “IP Configuration” for the network interface, we specify the ID of subnet (this tells Azure which subnet to place the network interface in) as well as the ID of the public IP address (to tell Azure to associate this public IP address with this network interface). We use the same resourceId function to get the IDs of the resources:

{
"type": "Microsoft.Network/publicIpAddresses",
"apiVersion": "2020-06-01",
"name": "myPip",
"location": "[resourceGroup().location]"
},
{
"type": "Microsoft.Network/networkInterfaces",
"apiVersion": "2020-06-01",
"name": "myNic",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Network/virtualNetworks/', 'myVnet')]",
"[resourceId('Microsoft.Network/publicIpAddresses/', 'myPip')]"
],
"properties": {
"ipConfigurations": [
{
"name": "myIpConfig",
"properties": {
"subnet": {
"id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'myVnet', 'mySubnet')]"
},
"publicIpAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', 'myPip')]"
}
}
}
]
}
},
...

Finally, we create a virtual machine using all of the resources above. We use “dependsOn” to make sure the network interface is complete before the virtual machine gets created. We then specify the ID of the network interface in the “networkProfile” so Azure knows to have this virtual machine use this network interface. We tell Azure to make this virtual machine of size “Standard_D2_v4”, which is a small general-purpose size (for more information on Azure virtual machine sizes, see this documentation: https://docs.microsoft.com/azure/virtual-machines/sizes). We also specify that the virtual machine should use the latest version of Ubuntu 18.04-LTS that is baked into the Azure platform (for a non-exhaustive list of images in Azure that gets updated daily, see this web page: https://negatblog.blob.core.windows.net/lists/os_image_list). Next, we specify the “computerName” (“name” is the name of the VM in the Azure platform, while “computerName” this is the name of the guest VM). Finally, we use the [parameters()] template function to pass the adminUsername and adminPassword parameters to the virtual machine (find more info on this template function in this documentation: https://docs.microsoft.com/azure/azure-resource-manager/templates/template-functions-deployment#parameters):

{
"type": "Microsoft.Compute/virtualMachines",
"apiVersion": "2020-06-01",
"name": "myVM",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Network/networkInterfaces', 'myNic')]"
],
"properties": {
"hardwareProfile": {
"vmSize": "Standard_D2_v4"
},
"storageProfile": {
"imageReference": {
"publisher": "Canonical",
"offer": "UbuntuServer",
"sku": "18.04-LTS",
"version": "latest"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', 'myNic')]"
}
]
},
"osProfile": {
"computerName": "myVM",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
}
}
}]}

After deploying this template, we can go to the Azure portal to find the public IP address that was created and SSH into our VM (note it might take a minute or two before the VM begins accepting ssh requests). And that’s it! That is our minimum viable ARM template :). Now let’s see how to modify this template to better suit specific scenarios. See the full list of incremental improvements to this template in the “Minimum Viable ARM Templates” page: https://negatblog.wordpress.com/minimum-viable-arm-templates/.

Leave a comment