Table of contents
Introduction: why and what
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 😓 ! 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 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, VMWare and Hyper-V all offer their own CLIs, but we'd like to use a provider-neutral tool. Hence we decided for Hashicorp's Vagrant, an open-source CLI-based VM manager already equipped with providers integrated with the main hypervisors.
This article is a quick start guide to create your first test environments using Vagrant.
Install and setup Vagrant
You can download Vagrant from the download page.
Once the installation is complete, you can verify it by opening your favorite shell (recommending Microsoft's open-source Windows Terminal), and running:
vagrant --version
Creating a new project
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
vagrant init --minimal
to initialize your Vagrant project in the local folder. It will create a minimal configuration file called 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:
Vagrant.configure("2") do |config|
config.vm.box = "base"
end
The Vagrant.configure
block sets a specific "configurator" 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") is a VM image - pretty much similar to the concept of Docker images - exportable from already created VMs or downloadable from the community's Vagrant Cloud repository.
Your first Vagrant up
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 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:
vagrant up
Since we didn't specify any virtualization provider, Vagrant will use Virtualbox as default: if you have a different hypervisor, make sure you edit the Vagrantfile with the appropriate settings. 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:
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 software. Therefore, we need to add in the first part of the Vagrantfile the following properties about the communicator (i.e. WinRM) that Vagrant will use to connect to our Windows operating system:
# 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 asweb
ordb
. ```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) ```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
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 and it can be attached to the vagrant up
command.
There are several provisioners integrated with Vagrant, such as Ansible, Puppet, Docker and even simply uploading files to the VM.
In our case, let's try to set up a development environment by installing a package manager (in our case Chocolatey) and then an IDE (what's better than Visual Studio Code?), and let's try to do it with the 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, please be sure to read Chocolatey's disclaimer about downloading scripts from the Internet ❗) in it:
In the config block of your Vagrantfile, add
# 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 from the Chocolatey community repository.
Conclusion
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! 🎉 🎊
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 you can find the code mentioned in this post.
See you next time! 👋
Top comments (2)
You can check this out: github.com/vaggeliskls/windows-in-...
Thank you.