Microsoft Dev Box – Make it your own

By | February 6, 2024

Recently we started deploying Dev Box to various divisions in the company, so I gave it a shot. Aside from being your typical VM-in-the-cloud, Dev Box offered a couple of interesting bonuses:

  • You can use the Windows Subsystem for Linux (WSL) and Docker
  • Different orgs/teams can deploy different “base” configurations/images to use as a starting point for Dev Boxes
  • Multiple monitor support

These are important pieces for developers being faced with using a virtual environment day-to-day!

If you create a standard VM in Azure, you can’t use WSL on it due to Hypervisor constraints, but the Dev Box product allows this which means you can have a Windows and a Linux dev environment in the cloud, running on as beefy a machine as you need! It means you can keep your local box lightweight and leave all the heavy stuff to your machine in the cloud.

The second one will be the focus of this blog post. There are numerous teams internally whose “onboarding” instructions involve extremely specific setup & configuration on a local dev box, not to mention cloning (literal) gigabytes of source code. Having this set up & available out-of-the-(dev)-box takes all the hassle out of getting started contributing on these teams. In this post, I plan to not only show you how to create a Dev Box environment yourself, but also give you step-by-step instructions on how to create a custom Dev Box image, allowing you to get straight to developing with all your favorite tools and anything else you want to have at-the-ready upon creation.

To accomplish it, a single PowerShell script orchestrates deployment of 2 Bicep templates and a few Azure CLI commands. The image building itself takes ~45 minutes, while the optimizing, validating, and deployment of the image then takes ~3 hours.

If you want to jump right to the code and give it a shot, you can visit the git repo.

The Basics

A Dev Box environment is created via an Azure Dev Center resource coupled with:

I was able to follow the quick start for Dev Center and the quick start for Dev Center Projects to get me started. I’m still not fully clear on what an “Environment” brings to the table but didn’t need one for this exercise so was happy to skip right over it.

Once you’ve set up your Dev Center, it’s pretty trivial to create a “Blank” dev box definition which just uses one of the numerous Azure Marketplace Windows images. But that’s only mildly better than simply spinning up a VM based on it. My goal was to figure out how to fully-customize an image so out-of-the-box it was ready to go for development with all our usual tools pre-installed.

Image Customization

What you’re about to read took me literal weeks’ worth of trial & error across a couple of different Azure tenants to test out various configurations.

Initially, I thought I would just spin up a VM, customize it by installing the software I wanted, etc. and then put it into a gallery for the Dev Center to pick up. I knew this would involve running a tool called sysprep on the machine to “Generalize” the image (only Generalized images are accepted by Dev Box). However, when attempting to go this route – which clearly would’ve been the simplest – I was met with numerous headaches at the sysprep step. It constantly failed, complaining I’d installed things at the User level instead of Machine level. If you read about how to work through this, it’s literally just uninstalling the offending program and try again – rinse & repeat until sysprep succeeds. But with Windows 11 (Client OS) & the Microsoft Store, it’s a whole ‘nuther world. One I just couldn’t get to work.

So, I started down the path of figuring out how to script the customization of these boxes, which led me to Azure VM Image Builder – something I didn’t know existed.

Image Builder lets you tell Azure to start with Image X, run script commands on that image (PowerShell for Windows, bash for Linux), and finally “deprovision” the image, which effectively runs sysprep and some other things. Since you’re scripting the image customization using the System account (more on this later), you’re effectively guaranteeing that sysprep works to generalize the image when you’re done. The fun comes in figuring out all the nuances of the base image, realizing just how much of what you want to customize is a User thing, and devising ways around it.

As you can imagine, the list of things I wanted done (and eventually figured out how to do) led me down lots of interesting paths. Some of the software being installed only has user installers available (looking at you, Postman), other pieces required elevation to run at all (Dev Drives), and others required to be run as user to be effective the way I wanted them to be (mounting Azure File shares). So, I had to get creative.

Caveats/Issues discovered

  • Custom PowerShell commands or scripts in Azure Image Builder can only be run as System Elevated, not User Elevated, and not non-elevated. When I tried running as the other two options, Image Builder would simply hang during the build altogether and had to be forcefully cancelled.
  • Winget is not preinstalled on the VS image. So, I had to install it. The problem is the PowerShell tasks in Image Builder run in Windows PowerShell, not PowerShell (Core). And the Winget module only works with PowerShell Core. So, I got to figure out how to shell out to pwsh from within PowerShell to run Winget commands. That was fun.
  • Image Builder templates cannot be updated – they must be completely deleted and re-deployed. This was a major pain in the ass as you can imagine. Which brings me to the next point
  • Using the “Provision File” task in Image Builder creates a static copy of the file on the image. If you change this file in some way, you must deploy a completely new version of the template for the change to take effect. For this reason, I recommend only using the Provision File task on files you don’t anticipate needing to change. For everything else, clone git repos.

Workarounds Developed

  • Getting things to run in the User Context I finally settled on accomplishing via Scheduled Tasks. I created these tasks in my provisioning scripts. When they run, they simply execute another PowerShell script with all the stuff I need done in the user context. You can configure a Scheduled task to run as elevated or not, so it covered both the bases. For a ‘one-time’ user setup (installing software, configuring Windows, etc.) my scheduled task just disables itself when it’s done. I had attempted doing this set of work via the HKLM\RunOnce registry key, but it wasn’t behaving as I thought it would.
  • I put all my scripts, initially, in a GitHub gist and just cloned it to c:\scripts as a PowerShell task in the image builder. This way, every time I ran the Build, I got the latest scripts. It made iterating a lot easier. As my solution grew into a full one-click deployment and I wanted to sort things into folders, I had to evolve it to a full GitHub repo but just changed the template to clone from there.
  • I got good at pwsh -c which is how you run a command with PowerShell Core. Spoiler: any time you want to do this, you also must tell it to run in Multithreaded Apartment mode using the -MTA flag else your command will likely fail. Another spoiler, you can’t pass objects back to the calling PowerShell instance, only strings. So, you’ll have to ConvertTo-Json and ConvertFrom-Json a lot if you plan to use pwsh to get objects back to the calling PowerShell instance.

Other tips

  • Winget is your friend. Its biggest bonus is that it also updates Windows Store apps – so, a simple winget update --all as a one-time Scheduled Task will take care of that piece of fluff we all must do when we set up a fresh box. Initially I started out with chocolatey but abandoned it in favor of Winget for this reason. However, you may find that Winget doesn’t have the app/tool you’re trying to install, whereas chocolatey likely will.
  • System installers make life a lot easier. Being able to install within Packer during Image Build is far easier than having to “hack” installing at the user context. Unfortunately, this kind of runs counterintuitive to (IMO) best practices. But, since Dev Boxes are only used by one user, it’s effectively equivalent.
  • Test out your script on a bare VM. It’s easy to spin up a VM in Azure based on the same image, then test your scripts there. You can’t test the system-context side of things, but you can test the user-context side. Figuring out what commands require elevation and what commands don’t. What things might need to be a logon task and what things persist across sessions, reboots, etc.

The Result

Upon running the script in the git repo linked at the top of this post, you’ll have a Dev Center in Azure with Dev Box pools in three different regions

Each region has two different images available – a “Blank” image that uses the vanilla marketplace image, and the customized (“DevReady”) image which builds off the Blank with the following customizations:

  1. Registry tweaks to forward time zone settings to the VM automatically.
  2. Optimization tweaks to Windows based on Azure Virtual Desktop best practices.
  3. Creates a 100GB Dev Drive for dev usage, setting the environment variables for npm, vc, nuget, pip, cargo and maven to use it.
  4. PowerShell (Core)
  5. Installs Windows Updates & Reboots, then
  6. The following tools/apps system-wide:
    • VS Code
    • Docker Desktop
    • Golang
    • Notepad++
    • GPG4Win
    • Windows Power Toys
    • Azure Storage Explorer
    • Python 3
  7. Configures logon tasks to Mount the created Dev Drive and a remote Azure Files share
  8. Disables ‘Reserved Storage’
  9. Configures a one-time script that will run upon first user logon, for things that require user-context installations/tweaks including:
    1. Installing Ubuntu to WSL2
    2. Installing the following tools/apps at the User level:
      • 7zip
      • Microsoft Dev Home with GitHub and Azure DevOps extensions
      • Terraform
      • Postman
    3. Updating installed packages (Microsoft Store and otherwise) via Winget
    4. Cleans up the Taskbar by
      • Unpinning the Microsoft Store
      • Removing the ‘Task View’ button
      • Removing the ‘Chat’ button (Teams for Personal)
      • Hiding the ‘Search’ box & button altogether
    5. Configures the Start menu to:
      • Don’t show recently added apps
      • Show most used apps
      • Show recently opened items
      • Don’t show recommendations
    6. Configures the User Theme to:
      • Enable Dark Mode
      • Turn on transparency effects
      • Show accent color on Start & Taskbar
      • Show accent color on Title bars & Window borders
      • Set accent color to Automatic
    7. Configures Multitasking settings to:
      • Don’t show Edge Tabs in the Alt+Tab view
      • Enable the Window Shake gesture
    8. Sets up other Windows Settings:
      • Save & Restart apps after reboot
      • Show protected operating system files
      • Turn on ‘End Task’ for Taskbar icons

All-in-all I found the exercise both frustrating and rewarding. My current team works with customers who often require everybody to work off VDIs (Virtual Desktop Interfaces) and, being a global team, the latency can often be unbearable. Dev Box allows you to spin up pools in any one of 13 (currently available) regions, making it easy to have a pool close to whomever will be using it. I plan to demonstrate this solution to these customers soon in hopes of making everybody’s lives better!

If a template developer box is something that could make you or your team’s lives easier, I encourage you to clone/fork the repo and get started with Microsoft Dev Box!