How Do You Create an Ubuntu Linux VM in Azure Using PowerShell Azure?

Updated 1/12/19

Problem scenario

You want to create an Ubuntu Linux server with one vCPU and one GB of RAM in Azure using PowerShell.  You want to use Azure Resource Manager (an ARM template).  You do not want to use the Azure Portal for 95% of the work involved.  What do you do to automate the process (including the deployment of a resource group, a network security group, a subnet) so you can have a VM that is ready to use?

Solution
Warning: This will create a VM in your Azure account.  It will cost you money to do this.

Prerequisites
You have a way of running Azure PowerShell commands.  If you need assistance with this, see this posting if your workstation is running Windows 7 or this posting if your workstation is running Windows 10.  You can log into the Azure Portal at the end (to set the password of the administrator user that you create when you create the VM with this process).

Procedures
1.  Save this PowerShell script to your desktop (with its first line being "<#").

<# 
 .SYNOPSIS
    Deploys a template to Azure

 .DESCRIPTION
    Deploys an Azure Resource Manager template

 .PARAMETER subscriptionId
    The subscription id where the template will be deployed.

 .PARAMETER resourceGroupName
    The resource group where the template will be deployed. Can be the name of an existing or a new resource group.

 .PARAMETER resourceGroupLocation
    Optional, a resource group location. If specified, will try to create a new resource group in this location. If not specified, assumes resource group is existing.

 .PARAMETER deploymentName
    The deployment name.

 .PARAMETER templateFilePath
    Optional, path to the template file. Defaults to template.json.

 .PARAMETER parametersFilePath
    Optional, path to the parameters file. Defaults to parameters.json. If file is not found, will prompt for parameter values based on template.
#>

param(
 [Parameter(Mandatory=$True)]
 [string]
 $subscriptionId,

 [Parameter(Mandatory=$True)]
 [string]
 $resourceGroupName,

 [string]
 $resourceGroupLocation,

 [Parameter(Mandatory=$True)]
 [string]
 $deploymentName,

 [string]
 $templateFilePath = "template.json",

 [string]
 $parametersFilePath = "parameters.json"
)

<#
.SYNOPSIS
    Registers RPs
#>
Function RegisterRP {
    Param(
        [string]$ResourceProviderNamespace
    )

    Write-Host "Registering resource provider '$ResourceProviderNamespace'";
    Register-AzureRmResourceProvider -ProviderNamespace $ResourceProviderNamespace;
}

#******************************************************************************
# Script body
# Execution begins here
#******************************************************************************
$ErrorActionPreference = "Stop"

# sign in
Write-Host "Logging in...";
Login-AzureRmAccount;

# select subscription
Write-Host "Selecting subscription '$subscriptionId'";
Select-AzureRmSubscription -SubscriptionID $subscriptionId;

# Register RPs
$resourceProviders = @("microsoft.compute","microsoft.storage","microsoft.network");
if($resourceProviders.length) {
    Write-Host "Registering resource providers"
    foreach($resourceProvider in $resourceProviders) {
        RegisterRP($resourceProvider);
    }
}

#Create or check for existing resource group
$resourceGroup = Get-AzureRmResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue
if(!$resourceGroup)
{
    Write-Host "Resource group '$resourceGroupName' does not exist. To create a new resource group, please enter a location.";
    if(!$resourceGroupLocation) {
        $resourceGroupLocation = Read-Host "resourceGroupLocation";
    }
    Write-Host "Creating resource group '$resourceGroupName' in location '$resourceGroupLocation'";
    New-AzureRmResourceGroup -Name $resourceGroupName -Location $resourceGroupLocation
}
else{
    Write-Host "Using existing resource group '$resourceGroupName'";
}

# Start the deployment
Write-Host "Starting deployment...";
if(Test-Path $parametersFilePath) {
    New-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroupName -TemplateFile $templateFilePath -TemplateParameterFile $parametersFilePath;
} else {
    New-AzureRmResourceGroupDeployment -ResourceGroupName $resourceGroupName -TemplateFile $templateFilePath;
}
# Last line of PowerShell script.

2.  Save these template.json and parameters.json files to the same folder on your workstation. 

parameters.json has the content starting below with "{" and ending with "}"

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "location": {
            "value": "eastus2"
        },
        "virtualMachineName": {
            "value": "nov16"
        },
        "virtualMachineSize": {
            "value": "Standard_B1s"
        },
        "adminUsername": {
            "value": "cooluser"
        },
        "virtualNetworkName": {
            "value": "changethis-vnet"
        },
        "networkInterfaceName": {
            "value": "nov1646"
        },
        "networkSecurityGroupName": {
            "value": "nov16-nsg"
        },
        "adminPassword": {
            "value": "c00lp@ssW0rd3
"        },
        "diagnosticsStorageAccountName": {
            "value": "changethisdiag604"
        },
        "diagnosticsStorageAccountType": {
            "value": "Standard_LRS"
        },
        "diagnosticsStorageAccountId": {
            "value": "Microsoft.Storage/storageAccounts/changethisdiag604"
        },
        "addressPrefix": {
            "value": "10.1.0.0/24"
        },
        "subnetName": {
            "value": "default"
        },
        "subnetPrefix": {
            "value": "10.1.0.0/24"
        },
        "publicIpAddressName": {
            "value": "nov16-ip"
        },
        "publicIpAddressType": {
            "value": "Dynamic"
        },
        "publicIpAddressSku": {
            "value": "Basic"
        }
    }
}

template.json starts with below "{" and ends with "}"

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "location": {
            "type": "string"
        },
        "virtualMachineName": {
            "type": "string"
        },
        "virtualMachineSize": {
            "type": "string"
        },
        "adminUsername": {
            "type": "string"
        },
        "virtualNetworkName": {
            "type": "string"
        },
        "networkInterfaceName": {
            "type": "string"
        },
        "networkSecurityGroupName": {
            "type": "string"
        },
        "adminPassword": {
            "type": "securestring"
        },
        "diagnosticsStorageAccountName": {
            "type": "string"
        },
        "diagnosticsStorageAccountId": {
            "type": "string"
        },
        "diagnosticsStorageAccountType": {
            "type": "string"
        },
        "addressPrefix": {
            "type": "string"
        },
        "subnetName": {
            "type": "string"
        },
        "subnetPrefix": {
            "type": "string"
        },
        "publicIpAddressName": {
            "type": "string"
        },
        "publicIpAddressType": {
            "type": "string"
        },
        "publicIpAddressSku": {
            "type": "string"
        }
    },
    "variables": {
        "vnetId": "[resourceId('changethis','Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]",
        "subnetRef": "[concat(variables('vnetId'), '/subnets/', parameters('subnetName'))]"
    },
    "resources": [
        {
            "name": "[parameters('virtualMachineName')]",
            "type": "Microsoft.Compute/virtualMachines",
            "apiVersion": "2016-04-30-preview",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[concat('Microsoft.Network/networkInterfaces/', parameters('networkInterfaceName'))]",
                "[concat('Microsoft.Storage/storageAccounts/', parameters('diagnosticsStorageAccountName'))]"
            ],
            "properties": {
                "osProfile": {
                    "computerName": "[parameters('virtualMachineName')]",
                    "adminUsername": "[parameters('adminUsername')]",
                    "adminPassword": "[parameters('adminPassword')]"
                },
                "hardwareProfile": {
                    "vmSize": "[parameters('virtualMachineSize')]"
                },
                "storageProfile": {
                    "imageReference": {
                        "publisher": "Canonical",
                        "offer": "UbuntuServer",
                        "sku": "16.04-LTS",
                        "version": "latest"
                    },
                    "osDisk": {
                        "createOption": "fromImage",
                        "managedDisk": {
                            "storageAccountType": "Standard_LRS"
                        }
                    },
                    "dataDisks": []
                },
                "networkProfile": {
                    "networkInterfaces": [
                        {
                            "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('networkInterfaceName'))]"
                        }
                    ]
                },
                "diagnosticsProfile": {
                    "bootDiagnostics": {
                        "enabled": true,
                        "storageUri": "[reference(resourceId('changethis', 'Microsoft.Storage/storageAccounts', parameters('diagnosticsStorageAccountName')), '2015-06-15').primaryEndpoints['blob']]"
                    }
                }
            }
        },
        {
            "name": "[parameters('diagnosticsStorageAccountName')]",
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2015-06-15",
            "location": "[parameters('location')]",
            "properties": {
                "accountType": "[parameters('diagnosticsStorageAccountType')]"
            }
        },
        {
            "name": "[parameters('virtualNetworkName')]",
            "type": "Microsoft.Network/virtualNetworks",
            "apiVersion": "2017-08-01",
            "location": "[parameters('location')]",
            "properties": {
                "addressSpace": {
                    "addressPrefixes": [
                        "[parameters('addressPrefix')]"
                    ]
                },
                "subnets": [
                    {
                        "name": "[parameters('subnetName')]",
                        "properties": {
                            "addressPrefix": "[parameters('subnetPrefix')]"
                        }
                    }
                ]
            }
        },
        {
            "name": "[parameters('networkInterfaceName')]",
            "type": "Microsoft.Network/networkInterfaces",
            "apiVersion": "2016-09-01",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[concat('Microsoft.Network/virtualNetworks/', parameters('virtualNetworkName'))]",
                "[concat('Microsoft.Network/publicIpAddresses/', parameters('publicIpAddressName'))]",
                "[concat('Microsoft.Network/networkSecurityGroups/', parameters('networkSecurityGroupName'))]"
            ],
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "ipconfig1",
                        "properties": {
                            "subnet": {
                                "id": "[variables('subnetRef')]"
                            },
                            "privateIPAllocationMethod": "Dynamic",
                            "publicIpAddress": {
                                "id": "[resourceId('changethis','Microsoft.Network/publicIpAddresses', parameters('publicIpAddressName'))]"
                            }
                        }
                    }
                ],
                "networkSecurityGroup": {
                    "id": "[resourceId('changethis', 'Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
                }
            }
        },
        {
            "name": "[parameters('publicIpAddressName')]",
            "type": "Microsoft.Network/publicIpAddresses",
            "apiVersion": "2017-08-01",
            "location": "[parameters('location')]",
            "properties": {
                "publicIpAllocationMethod": "[parameters('publicIpAddressType')]"
            },
            "sku": {
                "name": "[parameters('publicIpAddressSku')]"
            }
        },
        {
            "name": "[parameters('networkSecurityGroupName')]",
            "type": "Microsoft.Network/networkSecurityGroups",
            "apiVersion": "2017-06-01",
            "location": "[parameters('location')]",
            "properties": {
                "securityRules": [
                    {
                        "name": "default-allow-ssh",
                        "properties": {
                            "priority": 1000,
                            "protocol": "TCP",
                            "access": "Allow",
                            "direction": "Inbound",
                            "sourceAddressPrefix": "*",
                            "sourcePortRange": "*",
                            "destinationAddressPrefix": "*",
                            "destinationPortRange": "22"
                        }
                    }
                ]
            }
        }
    ],
    "outputs": {
        "adminUsername": {
            "type": "string",
            "value": "[parameters('adminUsername')]"
        }
    }
}

While these files (parameters.json and template.json) need to be in the same folder as each other, they need not be in the same folder as the PowerShell script (in step #1 above).  If you want the administrator account to have a name different from "azubuntu", change it in the parameters.json file.  You will set the password later, so you do not need to worry about that.  This solution will create the VM in Azure region "East US 2."  If you want to change this, replace "eastus2" with the region of your choice in the parameters.json file.***  The resource group that will subsume the VM can be in a different region from the VM's region.  The resource group's location will be configured in step #8.

3.  Open PowerShell ISE.

4.  Open the PowerShell script referred to in step #1.

5.  From the PS prompt (e.g., PS C:\Users\jdoe>), change to the directory path where you saved the template.json and parameters.json files (referred to in step #2).

6.  Have these things handy: credentials to your Azure Portal account and your Azure subscription ID.*

7.  Run the script by pressing the green arrow button that looks like a "Play" button.

8.  Respond to the interactive prompts.

    i.   Enter the Subscription ID.  
  ii.  The resource group name you enter must be "changethis" unless you went into the template.json and parameters.json and replaced "changethis" with a new string.**  If this is your first time running this PowerShell script and the name of the resource group (e.g., "changethis") is new, you will be prompted for a location in Azure to place this resource group. When you are prompted for "resourceGroupLocation" there are options in a different formats that will work.***  
  iii. For the deploymentname, you can make something up (e.g., acoolname).

9.  You may have to wait five minutes while it creates.  When the deployment is finished you will have a VM named "nov16."  You should now change the password of the administrator account of the VM.  Log into the Azure Portal. 

Open the CLI by clicking on the ">_" symbol in the upper right hand corner.  The lower part of the screen will become a dark blue.  Click "PowerShell (Windows)", and then click "Create storage".  Wait a few minutes for the initialization process to complete.  The upper left hand portion of the lower screen should say "PowerShell."

Prepare a draft of this command below by doing the replacing "azubuntu" with the username (if you changed the username in step #2), replacing "changethis" with the name of the resource group (unless you never changed it in step #8), replacing "coolPassword" with the password you want the user to have:

az vm user update --resource-group changethis --name nov16 --username azubuntu --password coolPassword

#Run this command in the PowerShell CLI prompt in the Azure Portal.

To quote Azure PowerShell "The supplied password must be between 6-72 characters long and must satisfy at least 3 of password complexity requirements from the following: 1) Contains an uppercase character 2) Contains a
lowercase character 3) Contains a numeric digit 4) Contains a special character 5) Control characters are not allowed."

10.  Now you should be able to log into the new machine "nov16" with the username and password there were used in step #9.

11.  If you want to re-use this procedure,  Modify the parameters.json file in two ways as described in i. and ii. below.

     i. Replace "nov16" in with the name you want to give to the VM (the hostname).
  ii. Change the "diag604" values to something else in the diagnosticsStorageAccountName's value and the diagnosticsStorageAccountId's value.  Save the file.  Here is an excerpt from the file as an example after the values have been changed:

 "diagnosticsStorageAccountName": {
            "value": "changethisdiag605a"
        },
        "diagnosticsStorageAccountType": {
            "value": "Standard_LRS"
        },
        "diagnosticsStorageAccountId": {
            "value": "Microsoft.Storage/storageAccounts/changethisdiag605a"

12.  You are now done.

* If you need assistance with determining your subscription ID, see this posting.

**  If you do not want to create a new resource group named "changethis", stop the PowerShell script and do these two things:

  1.  Go to the template.json file and replace all instances of "changethis" with the name of your choice.  
  2.  Go to the parameters.json file and replace all instances of "changethis" with the name of your choice.  

*** To find out potential regions (and the VM sizes that are correspondingly available), run these Azure PowerShell command:
# Use Login-AzureRmAccount if you have not logged in yet.  Then run these commands:
$list = Get-AzureRmLocation
echo $list.DisplayName

In the future we want to re-write these directions to use write-verbose instead of write-host. We want to warn people that write-verbose is recommended instead of write-host (according to this posting).

Leave a comment

Your email address will not be published. Required fields are marked *