A year ago in this post we created an Azure Virtual Machine using PowerShell instead of using the Azure Portal. This is fairly simple and allows a level of automation. But ARM Templates are simpler and in my opinion easier to source control.

Today lets create the same Virtual Machine, but using ARM this time.

First, fire up a PowerShell console and login to your subscription:

Login-AzureRmAccount
1. Create your ARM Template file

Fire up your favourite editor, Visual Studio Code or Visual Studio itself are my two choices.

Create a new JSON file and name it new-vm.json

Copy and paste the following structure into the new file. This is the beginning of our ARM Template.

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
  },
  "variables": {
  },
  "resources": [
  ],
  "outputs": {
  }
}

2. Create PowerShell Script that will run our ARM Template

This of course can be done just in the PowerShell command line, but it’s much faster and more repeatable to create the script file and be able to run it multiple times without having to type these things out.

Create a new file called deploy-new-vm.ps1

3. Create a Resource Group

Just like last time we need to create a Resource Group, this is the logical place to group all the elements that we’re going to create. This needs to be done in PowerShell, so let’s add the following to our new deploy-new-vm.ps1 file.

We also need to select the subscription that we want to put this new Resource Group into, you can get a list of all your subscriptions by using the Get-AzureRmSubscription command, choose which you want and copy the Id and enter into the $subscriptionId below

$subscriptionId = "<YOUR_SUBSCRIPTION_ID>"
Select-AzureRmSubscription -SubscriptionId $subscriptionId

$resourceGroup = "test-infra"
$location = "North Europe"

New-AzureRmResourceGroup -Name $resourceGroup -Location $location
4. Create storage account

Finally something that we can add to our new ARM Template.

First, add the following parameter to the “parameters” part of the template

"parameters": {
    "storageName": {
      "type": "string",
      "defaultValue": "teststorage",
      "metadata": {
        "description": "Name for our storage account"
      }
    }
  }

Now add a new resource, which is our storage account.

 "resources": [
    {
      "name": "[concat(parameters('storageName'), uniqueString(resourceGroup().id))]",
      "type": "Microsoft.Storage/storageAccounts",
      "location": "[resourceGroup().location]",
      "apiVersion": "2015-06-15",
      "properties": {
        "accountType": "Standard_LRS"
      }
    }
  ]

This is the same as our storage we created in the previous post.

We take the storageName which is a parameter and we add a unique string which is generated by uniqueString(resourceGroup().id) this means we can run this multiple times, and also that we won’t clash with anybody else’s storage accounts in the Azure cloud.

The location is defined from where the resourcegroup is located by using [resourceGroup().location]

The storage account type is still the Standard Locally Redundant Storage.

The full file will now look like so:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "storageName": {
      "type": "string",
      "defaultValue": "teststorage",
      "metadata": {
        "description": "Name for our storage account"
      }
    }
  },
  "variables": {
  },
  "resources": [
    {
      "name": "[concat(parameters('storageName'), uniqueString(resourceGroup().id))]",
      "type": "Microsoft.Storage/storageAccounts",
      "location": "[resourceGroup().location]",
      "apiVersion": "2015-06-15",
      "properties": {
        "accountType": "Standard_LRS"
      }
    }
  ],
  "outputs": {
  }
}

5. Create Virtual Network

The Virtual Machine needs to live in a Virtual Network, so we create a new Resource in the ARM Template

{
      "name": "test-net",
      "type": "Microsoft.Network/virtualNetworks",
      "location": "[resourceGroup().location]",
      "apiVersion": "2015-06-15",
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "10.0.0.0/16"
          ]
        },
        "subnets": [
          {
            "name": "frontendSubnet",
            "properties": {
              "addressPrefix": "10.0.1.0/24"
            }
          }
        ]
      }
    }
6. Setup IP Address and Network Interface

We need to give our new Virtual Machine a Network Interface and an IP address. This can be static, but we’re going to leave it as dynamic as per the last post.

Create a new resource for our Public IP address that we will use to RDP into the VM

 {
      "apiVersion": "2016-03-30",
      "type": "Microsoft.Network/publicIPAddresses",
      "name": "test-publicip",
      "location": "[resourceGroup().location]",
      "properties": {
        "publicIPAllocationMethod": "Dynamic"        
      }
    }

Lets create a parameter for our VM Name

"vmName": {
      "type": "string",
      "defaultValue": "testvm1",
      "metadata": {
        "description": "Name for our Virtual Machine"
      }
    }

Now let’s create a new resource for our new Network Interface

{
      "name": "[concat(parameters('vmName'),'-nic0')]",
      "type": "Microsoft.Network/networkInterfaces",
      "location": "[resourceGroup().location]",
      "apiVersion": "2017-06-01",
      "properties": {
        "ipConfigurations": [
          {
            "name": "ipconfig",
            "properties": {
              "privateIPAllocationMethod": "Dynamic",
              "publicIPAddress": {
                "id": "[resourceId('Microsoft.Network/publicIPAddresses','test-publicip')]"
              },
              "subnet": {
                "id": "[concat(resourceId('Microsoft.Network/virtualNetworks', 'test-net'),'/subnets/','frontendSubnet')]"
              }
            }
          }
        ]
      }
    }

You can see here, we are using the concat keyword to create a name for our Network Interface prefix of the VM Name and then adding -nic0

7. Create our Virtual Machine
{
      "name": "[concat(parameters('vmName'))]",
      "type": "Microsoft.Compute/virtualMachines",
      "location": "[resourceGroup().location]",
      "apiVersion": "2017-03-30",
      "dependsOn": [
        "[concat('Microsoft.Network/networkInterfaces/',parameters('vmName'),'-nic0')]"
      ],
      "properties": {
        "hardwareProfile": {
          "vmSize": "Basic_A1"
        },

        "osProfile": {
          "computerName": "[parameters('vmName')]",
          "adminUsername": "YOURUSERNAME",
          "adminPassword": "YOUR_PASSWORD12345678"
        },
        "storageProfile": {
          "imageReference": {
            "publisher": "MicrosoftWindowsServer",
            "offer": "WindowsServer",
            "sku": "2012-R2-Datacenter",
            "version": "latest"
          },
          "osDisk": {
            "osType": "Windows",
            "name": "[concat(parameters('vmName'),'-','osdisk')]",
            "createOption": "FromImage",
            "caching": "ReadWrite"
          }
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[concat(resourceId('Microsoft.Network/networkInterfaces',concat(parameters('vmName'))),'-nic0')]"
            }
          ]
        }
      }
    }

The dependsOn value in this resource means that it will wait until the provisioning of the specified resource before beginning creating this current resource. You can depend on multiple resources. In this case we are depending on the Network Interface being setup before we continue to create the Virtual Machine.

8. Deploy the ARM Template

Let’s add the deployment of the ARM Template to our deploy-new-vm.ps1 script

New-AzureRmResourceGroupDeployment `
    -Name test-infra-deployment `
    -ResourceGroupName $resourceGroup `
    -TemplateFile new-vm.json `
    -Verbose -Force
The full solution

Our final ARM Template json file (new-vm.json) looks like this:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "storageName": {
      "type": "string",
      "defaultValue": "teststorage",
      "metadata": {
        "description": "Name for our storage account"
      }
    },
    "vmName": {
      "type": "string",
      "defaultValue": "testvm1",
      "metadata": {
        "description": "Name for our Virtual Machine"
      }
    }
  },
  "variables": {
  },
  "resources": [
    {
      "name": "[concat(parameters('storageName'), uniqueString(resourceGroup().id))]",
      "type": "Microsoft.Storage/storageAccounts",
      "location": "[resourceGroup().location]",
      "apiVersion": "2015-06-15",
      "properties": {
        "accountType": "Standard_LRS"
      }
    },
    {
      "name": "test-net",
      "type": "Microsoft.Network/virtualNetworks",
      "location": "[resourceGroup().location]",
      "apiVersion": "2015-06-15",
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "10.0.0.0/16"
          ]
        },
        "subnets": [
          {
            "name": "frontendSubnet",
            "properties": {
              "addressPrefix": "10.0.1.0/24"
            }
          }
        ]
      }
    },
    {
      "apiVersion": "2016-03-30",
      "type": "Microsoft.Network/publicIPAddresses",
      "name": "test-publicip",
      "location": "[resourceGroup().location]",
      "properties": {
        "publicIPAllocationMethod": "Dynamic"
      }
    },
    {
      "name": "[concat(parameters('vmName'),'-nic0')]",
      "type": "Microsoft.Network/networkInterfaces",
      "location": "[resourceGroup().location]",
      "apiVersion": "2017-06-01",
      "properties": {
        "ipConfigurations": [
          {
            "name": "ipconfig",
            "properties": {
              "privateIPAllocationMethod": "Dynamic",
              "publicIPAddress": {
                "id": "[resourceId('Microsoft.Network/publicIPAddresses','test-publicip')]"
              },
              "subnet": {
                "id": "[concat(resourceId('Microsoft.Network/virtualNetworks', 'test-net'),'/subnets/','frontendSubnet')]"
              }
            }
          }
        ]
      }
    },
    {
      "name": "[concat(parameters('vmName'))]",
      "type": "Microsoft.Compute/virtualMachines",
      "location": "[resourceGroup().location]",
      "apiVersion": "2017-03-30",
      "dependsOn": [
        "[concat('Microsoft.Network/networkInterfaces/',parameters('vmName'),'-nic0')]"
      ],
      "properties": {
        "hardwareProfile": {
          "vmSize": "Basic_A1"
        },

        "osProfile": {
          "computerName": "[parameters('vmName')]",
          "adminUsername": "YOURUSERNAME",
          "adminPassword": "Your_PASSWORD12345678"
        },
        "storageProfile": {
          "imageReference": {
            "publisher": "MicrosoftWindowsServer",
            "offer": "WindowsServer",
            "sku": "2012-R2-Datacenter",
            "version": "latest"
          },
          "osDisk": {
            "osType": "Windows",
            "name": "[concat(parameters('vmName'),'-','osdisk')]",
            "createOption": "FromImage",
            "caching": "ReadWrite"
          }
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[concat(resourceId('Microsoft.Network/networkInterfaces',concat(parameters('vmName'))),'-nic0')]"
            }
          ]
        }
      }
    }
  ],
  "outputs": {
  }
}

And our Powershell file (deploy-new-vm.ps1):

$subscriptionId = "<YOUR_SUBSCRIPTION_ID>"
Select-AzureRmSubscription -SubscriptionId $subscriptionId

$resourceGroup = "test-infra"
$location = "North Europe"

New-AzureRmResourceGroup -Name $resourceGroup -Location $location

New-AzureRmResourceGroupDeployment `
    -Name test-infra-deployment `
    -ResourceGroupName $resourceGroup `
    -TemplateFile new-vm.json `
    -Verbose -Force

Deploy it!

Pull up a PowerShell prompt and make sure you have already run Login-AzureRmAccount, browse to your folder where our two new files are located and run them.

cd c:\dev\Create-VM-with-ARM\
.\deploy-new-vm.ps1

Github

Source files available on github

Comments

About

Founded in 2009, Odity is a software consultancy based in the United Kingdom.

Social Links

Our Bunker

United Kingdom.