Setting up Windows virtual test environments with Vagrant
Tags: vagrant, windows, tutorial
Published: 2021-08-19 07:12:43

## Table of contents
  * [Introduction](#intro)
  * [Install and setup Vagrant](#install)
    * [Creating a new project](#create)
    * [Your first `vagrant up`](#vagrantup)
    * [Customizing your VM](#customize)
  * [Provisioning](#provisioning)
  * [Conclusion](#conclusion)

## Introduction: why and what <a name="intro"></a>

Recently we've been trying to implement a Continuous Delivery pipeline for our in-house .NET applications suite - you can't imagine how long and painful it is to install and configure a multi-tier application by hand :sweat: ! Being a demanding task (a _looot_ of scripting required!), we needed to be able to quickly provision and destroy several test environments (where 'environment', containers not being supported yet, is a whole Windows Virtual Machine). 

The main idea was to create an easy, repeatable and trackable procedure. The best practices mentioned by [Google's DevOps capabilities](https://cloud.google.com/architecture/devops/devops-tech-deployment-automation) persuaded us to use **Infrastructure as Code (IaC)**. IaC is basically code - therefore version-controlled with `git` - made of creation and configuration scripts to be fed to a proper virtualization engine.

[Virtualbox](https://www.virtualbox.org/manual/ch08.html), [VMWare](https://www.vmware.com/support/ws5/doc/ws_learning_cli_vmrun.html) and [Hyper-V](https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/try-hyper-v-powershell) all offer their own CLIs, but we'd like to use a provider-neutral tool. Hence we decided for [Hashicorp's Vagrant](https://www.vagrantup.com), an [open-source](https://github.com/hashicorp/vagrant) CLI-based VM manager already equipped with providers integrated with the main hypervisors. 

<p align="center"> 
  <a href="https://www.vagrantup.com">
  <img width="3em" src="https://www.vagrantup.com/img/logo-hashicorp.svg">
</p>

This article is a quick start guide to create your first test environments using Vagrant. 

## Install and setup Vagrant <a name="install"></a>

You can download Vagrant from the [download page](https://www.vagrantup.com/downloads).

Once the installation is complete, you can verify it by opening your favorite shell (recommending Microsoft's open-source [Windows Terminal](https://github.com/microsoft/terminal)), and running:
```powershell
vagrant --version
```

### Creating a new project <a name="create"></a>

Vagrant depends on a specific location on your workstation to store all the configuration files of your project and the virtualized disks: therefore, you need to create your own `MyVagrant\` folder and `cd` into it.

Once you're in it, run
```powershell
vagrant init --minimal
```
to [initialize](https://www.vagrantup.com/docs/cli/init) your Vagrant project in the local folder. It will create a minimal configuration file called [Vagrantfile](https://www.vagrantup.com/docs/vagrantfile).

The Vagrantfile is a Ruby-based script containing the instructions Vagrant will use to interact with the various virtualization providers in order to create the described infrastructure.

The minimal Vagrantfile created with the `init` command is pretty simple:

```ruby
Vagrant.configure("2") do |config|
  config.vm.box = "base"
end
```

The `Vagrant.configure` block sets a specific ["configurator" version](https://www.vagrantup.com/docs/vagrantfile/version): all the next instructions will need to be included in this block.

The central command (`config.vm.box`) specifies the "box" used to create your Virtual Machine. A ["box"](https://www.vagrantup.com/docs/boxes)) is a VM image - pretty much similar to the concept of [Docker images](https://docs.docker.com/get-started/overview/#docker-objects) - exportable from already created VMs or downloadable from the community's [Vagrant Cloud repository](https://app.vagrantup.com/boxes/search).

### Your first `Vagrant up` <a name="vagrantup"></a>

If you try to spin up your VM with the initial Vagrantfile, Vagrant will return an error because it doesn't know any `"base"` box. You may replace that value with any box name available: in our example, we will try to boot a Windows Server machine, therefore we'll use the publicly available [Stefan Scherer's Windows Server 2019 box](https://app.vagrantup.com/StefanScherer/boxes/windows_2019) to have a 180-days trial version of Windows Server 2019 Eval with Desktop.

In case you don't have a local copy of the box, Vagrant will automatically download it from the Vagrant Cloud before creating the VM.

To boot your VM, just run:

```powershell
vagrant up
```

Since we didn't specify any virtualization provider, Vagrant will use [Virtualbox](https://www.virtualbox.org/) as default: if you have a different hypervisor, make sure you edit the Vagrantfile with the [appropriate settings](https://www.vagrantup.com/docs/providers/configuration). Remember then to add the `--provider=PROVIDER_NAME` (where PROVIDER_NAME is `vmware_fusion`, `hyperv`, `vmware_desktop`, `docker`, etc) to your `vagrant up` command.

Vagrant will take some minutes to download the image and boot up the VM, depending on the chosen OS. If your output includes:

```cmd
Timed out while waiting for the machine to boot. This means that
Vagrant was unable to communicate with the guest machine within
the configured ("config.vm.boot_timeout" value) time period.
```

The VM has correctly started on Virtualbox nonetheless, but Vagrant won't be able to communicate with it and proceed with the following instructions, such as [provisioning](#provisioning) software. Therefore, we need to add in the first part of the Vagrantfile the following properties about the [communicator](https://www.vagrantup.com/docs/vagrantfile/machine_settings#config-vm-communicator) (i.e. [WinRM](https://www.vagrantup.com/docs/vagrantfile/winrm_settings)) that Vagrant will use to connect to our Windows operating system:

```ruby
  # Additional parameters to communicate with Windows
  config.vm.boot_timeout = 60
  config.vm.communicator = "winrm"
  config.winrm.port = 55985
```

By opening the Virtualbox GUI you may find your new VM in the list: notice that, with a Vagrantfile as minimal as the one we provided, the Virtual Machine will have a combination of the provider's and Vagrant's default properties, such as

* The default VM name chosen by Vagrant follows the format `PROJECT-FOLDER_default_TIMESTAMP`; you may want to customize it with a more descriptive hostname, such as `web` or `db`.
```cmd
==> default: Setting the name of the VM: MyVagrant_default_1629122422277_92221
```
* The VM will have a main network adapter ("Adapter 1") connected via NAT to the host:
```cmd
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
```
* The network TCP port 22 on the host is mapped to TCP port 2222 on the guest: 
```cmd
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
```

We'll see how to change the properties above in the following section.

## Customizing your VM <a name="customize"></a>

Customizing our newborn Virtual Machine requires a little bit of [additional configuration](https://www.vagrantup.com/docs/providers/virtualbox/configuration) in the Vagrantfile: some settings needs to be added in the  `config.vm` namespace of the Vagrantfile. 

All the code described below must be added inside the `Vagrant.configure("2") do |config|` section.

* Some properties can be changed by configuring the `provider`, such as the [VM's name](https://www.vagrantup.com/docs/providers/virtualbox/configuration#virtual-machine-name) or the [hypervisor's GUI](https://www.vagrantup.com/docs/providers/virtualbox/configuration#gui-vs-headless). Notice that some settings don't have a specific shortcut and they will need the [`customize`](https://www.vagrantup.com/docs/providers/virtualbox/configuration#vboxmanage-customizations) property:
```ruby
# Customization
config.vm.provider "virtualbox" do |v|
  v.name = "my_vm"    # Sets the new VM's name
  v.gui = true        # Enables the hypervisor's GUI
  v.memory = 2048     # Sets the VM's RAM
  v.customize ["modifyvm", :id, "--draganddrop", "hosttoguest"] # Enables drag-and-drop between host and guest
  v.customize ["modifyvm", :id, "--clipboard", "bidirectional"] # Enables a bidirectional clipboard between host and guest
end
```

* Additional ports can be mapped between host and guest with the `config.vm.network` settings (additional details on networking can be found [here](https://www.vagrantup.com/docs/networking))
```ruby
  # Customization
  config.vm.network :forwarded_port, guest: 80, host: 8080, id: "http"  # Map host's port 8080 to guest's port 80
```

* By default, Vagrant will share (or ["sync"](https://www.vagrantup.com/docs/synced-folders)) your project directory - i.e. the directory with the Vagrantfile - to `/vagrant` on the guest. You can set additional synced folders with:
```ruby
  # Customization
  config.vm.synced_folder "app/wwwroot", "src/"  # Map the host's "src/" folder to the guest's "/app/wwwroot"
```

And many more!

After each change, remember to run `vagrant validate` to check the Vagrantfile for any syntax error.

If the validation is successful, run `vagrant reload` to reboot your VM and apply all the changes.

## Provisioning <a name="provisioning"></a>

So now we have a ready VM, but what if we need to also install software or change the configuration during the boot process? This practice is called [provisioning](https://www.vagrantup.com/docs/provisioning) and it can be attached to the `vagrant up` command.

There are several provisioners integrated with Vagrant, such as [Ansible](https://www.vagrantup.com/docs/provisioning/ansible), [Puppet](https://www.vagrantup.com/docs/provisioning/puppet_apply), [Docker](https://www.vagrantup.com/docs/provisioning/docker) and even simply uploading [files](https://www.vagrantup.com/docs/provisioning/file) to the VM.

In our case, let's try to set up a development environment by installing a package manager (in our case [Chocolatey](https://chocolatey.org/)) and then an IDE (what's better than [Visual Studio Code](https://code.visualstudio.com/)?), and let's try to do it with the [shell](https://www.vagrantup.com/docs/provisioning/shell) provisioner, i.e. uploading and running a shell script.

Create a `scripts/` folder in your project and add the `InstallChocolatey.ps1` PowerShell script (you may find it on the official [Chocolatey install page](https://chocolatey.org/install#individual), please be sure to read Chocolatey's disclaimer about downloading scripts from the Internet :heavy_exclamation_mark:) in it:

In the config block of your Vagrantfile, add

```ruby
  # Provisioning
  config.vm.provision "shell", path: "scripts/InstallChocolatey.ps1"  # Run the external script to install Chocolatey
  config.vm.provision "shell", inline: "choco install vscode --yes"   # Run the inline script to install VSCode via Chocolatey
```

Now, if you `vagrant reload` your VM, the provisioning will be performed at the end of the boot. The first shell script (called from an external file whose `path` is provided) will download and install Chocolatey, the second `inline` script will force the download and installation of the [Visual Studio Code's choco package](https://community.chocolatey.org/packages/vscode) from the Chocolatey community repository.

## Conclusion <a name="conclusion"></a>

In just a few minutes, we were able to spin up a Virtual Machine with our desired operating system and with some software installed (a package manager and an IDE) and now we're ready to write code and start some tests! :tada: :confetti_ball:

![Our new virtual test environment!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9f9yrgyjabqn9nhpc8fg.jpg)

<!-- ![WinServ2019](../devto-articles/img/vagrant-1.jpg "Our new virtual test environment!") -->

Plus, our IaC files (i.e. the Vagrantfile and the corresponding PowerShell scripts) now can be stored in a `git` repository, thus taking advantage of all the source control features: basically we have a **versioned infrastructure**, repeatable and exportable in any settings. 

[Here](https://gist.github.com/sannae/07cd210478336f6f8aa571636f33b06b) you can find the code mentioned in this post.

See you next time! :wave: