First steps with Windows on Vagrant
One thing that would be useful for one of my projects is an easy way to install IIS, PHP & SQL Server on Windows in a repeatable way. I wasn’t sure how to do this until I came across Chocolatey and Boxstarter. Chocolatey is a package manager for Windows along the same lines as yum and apt for Linux and knows how to install a remarkable amount of software. Boxstarter is a way to automate installations of Windows using Chocolatey packages.
This is the start of my journey and I’ve not yet reached the destination I’m hoping to get to.
The Windows Vagrant base box
To start out I need a Windows base box for Vagrant. There doesn’t seem to be an obvious frequently-updated Windows Server 2016 Vagrant box out there, so I turned to Matt Wrock‘s article Creating windows base images using Packer and Boxstarter which provides a great overview on what to do. He even provides a GitHub repository of packer-templates which do the hard work.
I forked his repo and tweaked a few bits and then ran packer on my Mac and it generated a windows2016min-virtualbox.box base box.
To install into Vagrant, you can add to the Vagrant Cloud so that it’s easily shareable, or you can just add to your local vagrant with:
$ vagrant box add --name {your-org-name}/windows2016 windows2016min-virtualbox.box
For myself, I named my box 19ft/windows2016.
Vagrantfile
The initial Vagrantfile is quite simple:
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Base box: https://github.com/akrabat/packer-templates
Vagrant.configure("2") do |config|
config.vm.box = "19ft/windows2016"
config.vm.guest = :windows
config.vm.boot_timeout = 600
config.vm.hostname = "WinDev"
config.vm.network "private_network", ip: "192.168.99.201"
config.vm.communicator = "winrm"
config.vm.provider "VirtualBox" do |vb|
# Display the VirtualBox GUI when booting the machine
vb.gui = true
# Customize the amount of memory on the VM:
vb.memory = "2048"
vb.cpus = 2
end
end
We can then start our VM:
$ vagrant up
This will fire up Virtualbox with Windows 2016 in a GUI that you can log into. There are two users: Administrator and Vagrant, both have the same password: vagrant.
You may want to turn on VirtalBox’s shared clipboard from the Devices menu.
The next step is to automate installing some software on it.
Automating software installation
We can use Chocolatey controlled by Boxstarter install useful software.
Firstly we need to add a provisioning script to our `Vagrantfile`:
config.vm.provision "shell", privileged: "true", inline: <<-'POWERSHELL'
Set-TimeZone "Coordinated Universal Time"
# Install Boxstarter
. { iwr -useb https://boxstarter.org/bootstrapper.ps1 } | iex; Get-Boxstarter -Force
# Copy setup.ps1 to the Temp directory and then run boxstarter with our setup.ps1 script
$env:PSModulePath = "$([System.Environment]::GetEnvironmentVariable('PSModulePath', 'User'));$([System.Environment]::GetEnvironmentVariable('PSModulePath', 'Machine'))"
cp C:\vagrant\setup.ps1 $env:TEMP
Import-Module Boxstarter.Chocolatey
$credential = New-Object System.Management.Automation.PSCredential("vagrant", (ConvertTo-SecureString "vagrant" -AsPlainText -Force))
Install-BoxstarterPackage $env:TEMP\setup.ps1 -Credential $credential
POWERSHELL
If we vagrant destroy the the current VM and then run vagrant up again, Vagrant will run our inline provisioning script which will install Boxstart which will then run setup.ps1
setup.ps1 is the script that contains the instructions for setting up Windows and installing various software. My initial one looks like this:
# Boxstarter setup script
# Notes:
# - This file has to be idempotent. it will be run several times if the
# computer needs to be restarted. When that happens, Boxstarter schedules
# this script to run again with an auto-logon. Fortunately choco install
# handles trying to install the same package more than once.
# - Pass -y to choco install to avoid interactive prompts
# Fix Windows Explorer
Set-WindowsExplorerOptions -EnableShowHiddenFilesFoldersDrives -EnableShowProtectedOSFiles -EnableShowFileExtensions -EnableShowFullPathInTitleBar
# Useful apps
choco install -y googlechrome
choco install -y firefox
choco install -y 7zip
choco install -y notepadplusplus
choco install git -y -params '"/GitAndUnixToolsOnPath /NoAutoCrlf"'
Install-ChocolateyPinnedTaskBarItem "${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe"
Install-ChocolateyPinnedTaskBarItem "${env:ProgramFiles(x86)}\Mozilla Firefox\firefox.exe"
# SQL Server
choco install -y sql-server-2017 --params='/SAPWD="Indigo12!" /SECURITYMODE=sql'
choco install -y sql-server-management-studio
# IIS
choco install -y IIS-WebServerRole -source windowsfeatures
choco install -y IIS-WebServer -source windowsfeatures
choco install -y IIS-CommonHttpFeatures -source windowsfeatures
choco install -y IIS-StaticContent -source windowsfeatures
choco install -y IIS-DefaultDocument -source windowsfeatures
choco install -y IIS-DirectoryBrowsing -source windowsfeatures
choco install -y IIS-HttpErrors -source windowsfeatures
choco install -y IIS-ApplicationDevelopment -source windowsfeatures
choco install -y IIS-CGI -source windowsfeatures
choco install -y IIS-HealthAndDiagnostics -source windowsfeatures
choco install -y IIS-HttpLogging -source windowsfeatures
choco install -y IIS-LoggingLibraries -source windowsfeatures
choco install -y IIS-RequestMonitor -source windowsfeatures
choco install -y IIS-Security -source windowsfeatures
choco install -y IIS-RequestFiltering -source windowsfeatures
choco install -y IIS-HttpCompressionStatic -source windowsfeatures
choco install -y IIS-WebServerManagementTools -source windowsfeatures
choco install -y IIS-ManagementConsole -source windowsfeatures
choco install -y IIS-ManagementScriptingTools -source windowsfeatures
choco install -y WAS-WindowsActivationService -source windowsfeatures
choco install -y WAS-ProcessModel -source windowsfeatures
choco install -y urlrewrite
# PHP
choco install -y php --package-parameters="/InstallDir:C:\PHP"
When the script runs it sets some configuration options on Windows Explorer and then install Chrome. Chrome requires a restart, so Boxstarter notes that a restart is required, so will store the information required in order to start up again after the reboot and then initiate the reboot.
Vagrant doesn’t handle this well and will fail with a message something like this:
Chocolatey installed 1/1 packages. 0 packages failed.
See the log for details (C:\ProgramData\chocolatey\logs\chocolatey.log).
Boxstarter: found C:\Users\vagrant\AppData\Local\Boxstarter\Boxstarter.1648.restart we are restarting
++ Boxstarter finished Calling Chocolatey to install tmpF47D.tmp. This may take several minutes to complete... 00:29:24.5097146
Boxstarter: writing restart file
Boxstarter: Restore Automatic Updates from Windows Update
Boxstarter: UAC Enabled. Disabling...
Boxstarter: Disabling UAC
Boxstarter: Securely Storing WINDEV\vagrant credentials for automatic logon
Boxstarter: Logon Set
Boxstarter: Restart Required. Restarting now...
Errors : {}
ComputerName : localhost
Completed : True
FinishTime : 11/12/2019 1:59:01 PM
StartTime : 11/12/2019 1:29:21 PM
Stderr from the command:
Don’t worry though, the VM will restart and Boxstarter will continue. As there’s no way to monitor this through Vagrant, having the GUI available is useful so we can watch it work. Boxstarter will the go on and install a Firefox, NotePad++, git, SQL Server, IIS and PHP for me with no intervention required.
Note that provisioning takes a long while. At the end though, we have Sql Server and IIS/PHP installed. We can then use vagrant package if we want to store the state at this point should we wish to.
All done
I’m a fan of scripted set up of infrastructure as I’m much more likely to be able to repeat it and share with team members. With Boxstarter and Chocolatey, it has been a nice surprise that I can build a Windows Vagrant VM mostly automatically in the same way I can with Linux.
It’s not pain-free though as it seems far less robust and sometimes I have to re-run the boxstarter script which is irritating, but not the end of the world.
The full Vagrantfile and setup.ps1 are in this gist.