As a consultant I work on at least a few projects at a time, and prefer to isolate my development environments by creating an Azure VM for each customer. Isolating the environments are great because I can focus on the software and setup I need for that customer, and will never be in a situation where VPN clients or different software versions clash with each other.

With my development environments in Azure I am truly mobile, can work from anywhere and can lose my working machine at any point without much impact beyond getting another one.

Isn’t that expensive?

Not as expensive as you may think. If you make a few smart choices and keep the machines shut down when you’re not using them, you’ll be surprised at how cost effective it can be. To give you an idea, I have about 5 VMs right now and are working on at least one of them every day (for at least 8 hours) and my Azure bill is less than $100 a month.

My setup

Creating virtual machines in the Azure portal are easy but tedious at the same time, if you’re like me. I can never remember the size, image and naming convention from the previous build and stepping through it each time takes a few. I’ve finally gotten around to automating the process, and now that I’ve streamlined everything with ARM templates I’ve reduced the time to get an environment up and running from a few hours to less than one. The bits and pieces I use to get everything up and running looks something like this:

  • ARM template to create the VM (plus a PowerShell script to execute it)
  • Chocolatey to install software unattended
  • mRemoteNG for remote access
  • Dropbox to backup and sync files to my local and/or other machines

ARM templates

ARM templates seem like a really good idea, until you try to create something from a template you’ve generated in the Azure portal. For virtual machines specifically, the generated template doesn’t work and the error messages are not that easy to decipher.

Why does the generated template not work? The main reason is the fact that the portal only scripts the creation of the VM itself, and not the other resources it depends on. At minimum, an Azure VM is dependent on the following resources:

  • Resource Group
  • Network Interface
    • Network Security Group
    • Virtual Network
      • Virtual Subnet

The template generated by the Azure portal assumes that the Resource Group and Network Interface (and its sub-resources) already exist, and fails gloriously when they don’t.

Once you figure that part out, the second thing that will likely trip you up (as it did me) is resource dependencies. ARM templates will try to create resources in parallel, and unless you note the dependencies in the template, resource creation might fail. A good example of this is virtual networks & subnets:

{
    "type": "Microsoft.Network/virtualNetworks",
    "apiVersion": "2020-11-01",
    "name": "MyVnet",
    "location": "West US 2",
    "properties": {
        "addressSpace": {
            "addressPrefixes": [
                "10.2.0.0/28"
            ]
        },
        "subnets": [
            {
                "name": "MySubnet",
                "properties": {
                    "addressPrefix": "10.2.0.0/28",
                    "delegations": [],
                    "privateEndpointNetworkPolicies": "Enabled",
                    "privateLinkServiceNetworkPolicies": "Enabled"
                }
            }
        ],
        "virtualNetworkPeerings": [],
        "enableDdosProtection": false
    }
}, 
{
    "type": "Microsoft.Network/virtualNetworks/subnets",
    "apiVersion": "2020-11-01",
    "name": "MySubnet",
    "dependsOn": [
        "[resourceId('Microsoft.Network/virtualNetworks', 'MyVnet')]"
    ],
    "properties": {
        "addressPrefix": "10.2.0.0/28",
        "delegations": [],
        "privateEndpointNetworkPolicies": "Enabled",
        "privateLinkServiceNetworkPolicies": "Enabled"
    }
}




In the snippet above, even though the virtual network has a section for the subnet (line 12 – 21), it will not create the subnet itself. You need to create the subnet separately (line 28 – 40) but ensure that the dependency is noted with the dependsOn property, which dictates the order of execution. I found this rather confusing at first, because it looks like you’re duplicating things…but that’s how it works and the creation of the virtual network will not succeed otherwise.

A few other things worth pointing out when it comes to ARM templates:

  • Use parameters for the things that you’d want to change when deploying the ARM template, but variables for things that will stay static. I opted to use variables to enforce naming conventions, machine size, OS image, etc.
  • Don’t try to create the resource group with the ARM template, because you need to specify the resource group when creating your deployment. In my opinion it is better to create that as the first and separate step in PowerShell.
  • For each resource in the ARM template, you have to specify the API version…and not all resources will always have the same version. Check the documentation to make sure you’re using a valid (and not outdated) version. Here’s the reference for VMs, where you’ll see the different API versions at the top of the page.

Parameters & variables

I use parameters for everything that might change, like the location of the VM and Azure subscription I’d like to deploy to. In addition to that I use variables to then derive the names of my resources and enforce naming conventions…the things I’d like to stay consistent. Here’s what that may look like:

...
...
"parameters": {
    "_SubscriptionId" : {
        "defaultValue": "00000000-0000-0000-0000-000000000000",
        "type": "String", 
        "metadata": {
            "description": "Azure Subscription Id."
        }
    },
    "_CustomerName": {
        "defaultValue": "TestCustomer",
        "type": "String", 
        "metadata": {
            "description": "Name of the customer. This will be used in a tag for easy reference."
        }
    },
    "_CustomerPrefix": {
        "defaultValue": "tc",
        "type": "String", 
        "metadata": {
            "description": "Customer prefix. This will be used in the names of resources for easy reference."
        }
    },
"variables": {
    "_VirtualMachineName": "[concat('28t-vm-', parameters('_CustomerPrefix'), '-dev')]",
    "_VirtualMachineSize": "Standard_D4as_v4", 
    "_ImageSku": "20h2-pron-g2", 
    "_OsDiskName": "[concat(variables('_VirtualMachineName'), '-osdisk-1')]", 
    "_DataDiskName": "[concat(variables('_VirtualMachineName'), '-datadisk-1')]", 
},
...
...




The references to variables, parameters or functions are enclosed in block quotes, and from the above you can see that the name of my VM will be “28t-vm-tc-dev” if my customer prefix is “tc”. I’ve chosen to prefix all my parameter and variable names with an underscore, but that’s just a personal preference (I feel it stands out more in the JSON) and not something that’s required.

Resources

The resources section of the template is where all the magic happens, and as long as you include the mandatory properties and dependencies it’s all just semantics. You may need to fiddle around a bit, but start small with one resource and build it up as you move along. The beauty of these templates are that they will update a resource if it already exists, using the new properties as defined by the template…so you don’t need to go and delete everything unless you change the names of the resources themselves.

A word of caution though: Be careful when deploying resources with minimal properties, because some default values could turn out to be rather expensive. When it comes to VMs for instance, the default storage it will create (if not specified in the template) is the expensive Premium SSD type. Virtual machines are also started as soon as they are created, and that could cost you if you’re not paying attention or not including a shutdown schedule.

Speaking of the shutdown schedule, it absolutely has to have the following naming convention or it just won’t work: shutdown-computevm-<vm name>

Deploying the template

You have the following options to deploy your template:

  • The Azure portal
  • Azure CLI
  • PowerShell

My preference is PowerShell, because I can script the creation of the resource group itself and perform many other tasks at the same time. Here’s a good Quickstart guide that shows the different options, but you can also use my PowerShell script as a starting point if you like.





Want to download my ARM template and PowerShell script to deploy it? Get it from my GitHub repo.

One thought on “Creating development environments in Azure, the easy way

Leave a Reply

Discover more from Martin's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading