Creating Dev box in azure, putting packer into good use

The laptop I’m currently writing this blog post in had a serious issue, it was laggy and slow making small actions a chore, opening a word document could take minutes, I didn’t even dare open visual studio, VsCode was all I can do, luckily for me azure cloud shell and the enabled me to survive, well, until I finally upgraded its hard disk to an SSD.

But in the middle of all this pain, I had an idea, why not build a virtual machine and configure it with all of my development tools, and remote into it and get freed from my laptop constraints, and that’s what I did. and here’s the recipe.

The First Ingredient, one teaspoon of Azure

To spin a virtual machine I decided to go with Azure, Azure provides compute service that allows you to spin Linux and Windows virtual machines in minutes, I can choose the size depending on the CPU, RAM I need for my cloud dev box. but to build the dev box image I need another tool.

Second Ingredient,one teaspoon of Packer

I went for packer, packer automates the creation of machine images, it uses the concept of builder with a configuration written in JSON to automate the build process, spinning the base image, installing the updates (if you want) and provisioning the image, all of that can be automated using packer.

Third Ingredient, a handful of PowerShell scripts

To provision and install my development tools on the image, we need to use packer script provisioner to execute the installation scripts, to make it easier, I am going to use chocolatey for the installation of the packages, chocolatey reduces the steps needed to install an app to just

choco install -y 'package-name'

the packages I am going to install are, Visual Studio, Git, Resharper, Insomnia (a great HTTP client), LINQPad, and localdb, installation commands for these packages are straight forward, for brevity I am going to skip the contents of scripts, but you can check the repo here.

Fourth Ingredient, one build template

packer takes a build template file defined in JSON, so we are going to create that

touch devBoxTemplate.json

I’m going to use azure builder to create the dev box image, this builder instruments azure through the ARM API, it spins up a virtual machine, using either a custom vhd file, or one of the marketplace images as the base image for the VM, then provision the image by applying the provisioners defined in the template, the configuration is as follows

"description" : "packer template for tracking my dev tools and creating azure image, and installing the list",
    "variables" : {
        "subscription_id" : "{{env `AZURE_SUBSCRIPTION_ID` }}"
    },
    "builders" :[{
        "type" : "azure-arm",
        "subscription_id" : "{{user `subscription_id`}}",
        "os_type" : "windows",
        "managed_image_name" : "devbox",
        "managed_image_resource_group_name" : "devbox-resource-group",
        "location" : "eastus",
        "image_publisher" : "MicrosoftWindowsDesktop",
        "image_offer" : "Windows-10",
        "image_sku" : "19h1-pro",
        "communicator": "winrm",
        "winrm_use_ssl": true,
        "winrm_insecure": true,
        "winrm_timeout": "5m",
        "winrm_username": "packer",
        "async_resourcegroup_delete" : true
    }],

you can see I am specifying azure-rm as the build type, to use azure builder, also I’m using one of the managed images on the azure market place, specified by the coordinates: image_publisher,image_offer and image_sku the image I am using here is a Windows 10 image, this will give us our base windows operating system, to install the dev tools we go to the provisiong part.

"provisioners": [
        {
            "type" : "powershell",
            "scripts" : [
                "Scripts/InstallChocolatey.ps1",
                "Scripts/InstallVS2019.ps1",
                "Scripts/InstallRider.ps1",
                "Scripts/InstallDotNetCore.ps1",
                "Scripts/InstallGit.ps1",
                "Scripts/InstallInsomnia.ps1",
                "Scripts/InstallLinqPad.ps1",
                "Scripts/InstallLocalDb.ps1"
            ]
        },
        {
            "type": "powershell",
            "inline": [
                " # NOTE: the following *3* lines are only needed if the you have installed the Guest Agent.",
                "  while ((Get-Service RdAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
                "  while ((Get-Service WindowsAzureTelemetryService).Status -ne 'Running') { Start-Sleep -s 5 }",
                "  while ((Get-Service WindowsAzureGuestAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
    
                "& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit",
                "while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10  } else { break } }"
            ]
        }
      ]

The first provisioner uses PowerShell and passes in the installation scripts, first of which installs chocolaty package manager, which is used by the subsequent scripts to install the actual tools. the final step is to deprovision the running VM before capturing it into an image, and that the functionality of the final inline PowerShell provisioner. at which stage packer will capture the state of the temporary VM into a final image.

Cooking step

once we have our build template ready, we just need to call packer’s build command

packer build ./devBoxTemplate.json

packer will spew up log status as the build progress, prefixed by the builder name, azure-arm in our case, and the end of the build process, packer will place the image under the resource group we specified by managed_image_resource_group_name.

Devbox Image