Simplify Code Deployment and Your Life with Vagrant

Several months ago I stumbled across a deployment automation tool called Vagrant.  Admittedly, I did not appreciate the value of what Vagrant had to offer at the time, but I have since become a fanatic for automation.  I nearly forgot about Vagrant until a few days ago when I encountered the kind of scenario that Vagrant was built to simplify.  I was on the hunt for a Selenium framework for node.js and settled on webdriverIO because I like the syntax of the API.  I started working my way through the getting started guide.  The getting started example is simple enough, just a few lines of code to get the contents of the <title> element from www.google.com with Selenium using firefox as the client.

var webdriverio = require('webdriverio')

var options = {
  desiredCapabilities: {
    browserName: 'firefox'
  }
}

webdriverio
  .remote(options)
  .init()
  .url('http://www.google.com')
  .getTitle().then(function (title) {
    console.log('title was: ' + title)
  })
  .end

This looks pretty simple.  But, there is a catch.  You need to start Selenium Standalone Server independently.  No biggie, following the directions in the getting started guide I launched Selenium

java -jar selenium-server-standalone-3.0.1.jar

And executed the code above.

node test.js
...
WARN - Exception: The path to the driver executable must be set by the webdriver.gecko.driver system property; for more information, see https://github.com/mozilla/geckodriver. The latest version can be downloaded from https://github.com/mozilla/geckodriver/releases

Well, dang.  But the error message seems pretty straightforward.  I snagged a copy of the geckodriver executable by following the link in the message, launched it, and executed the test code again.  Still the same error message.  Eventually I determined that the path to the executable needs to be set when launching Selenium Standalone.

java -jar -Dwebdriver.geckodriver.driver=./geckodriver selenium-server-standalone-3.0.1.jar

Note that in the example above, I am indicating that the path is in the directory I am launching Selenium from with -Dwebdriver.geckodriver.driver=./geckodriver, where ./geckodriver is the path to the executable.

The maintainers of webdriverIO make it simple to propose updates to their documentation by placing a call to action on each page that creates a pull request for whatever page you are viewing.  I wanted to share share what I had learned so that the next person did not stumble in the same place, but I needed to be be able to demonstrate how my solution was correct.  The trouble is, I had already fixed the problem on my machine.  The path to geckodriver only needs to be set once.

The initialization command for Vagrant immediately sprung to mind.

vagrant up

Vagrant makes it easy to spin up new instances of a virtual machine fully provisioned with exactly what you need.  Check out their getting started guide, which takes about 15 minutes to complete, and shows you how to provision a basic Ubuntu box running a simple apache file server with forwarded ports so that you can access the server from your host machine.

The great thing about Vagrant is that the configuration of your virtual machine is portable, and can be destroyed and recreated again and again.  All that you need is a install of Vagrant and Virtualbox on your host, a file to bootstrap the machine, and a Vagrantfile to define the machine.  Feel free to clone a copy of my virtual machine that I created for this project so that you can follow along.  There are instructions to install and run the machine in the readme.md file.

My goal was to specify the requirements for the virtual machines that would emulate the machine state of my current problem, and emulate the machine state required to solve the problem, and automate running the test so that anyone could download a copy of my solution and test it in a virtual machine with a single command.  Vagrant allows you to do exactly that.

After installing Vagrant and VirutalBox, the next step is to create a Vagrantfile and place it in your project directory.  You can create one yourself, or you can use the init utility by running it from your project directory.

vagrant init

I didn’t need anything more complicated than what was provided in Vagrant’s getting started guide.

Vagrant.configure("2") do |config|
  config.vm.box = "hashicorp/precise64"
  config.vm.provision :shell, path: "./bootstrap.sh"
  config.vm.network :forwarded_port, guest: 80, host: 4567
end

Vagrantfiles use Ruby syntax, but you don’t need to know much (or anything) about Ruby to get started.  In the code above, I am declaring what the configuration of the machine is.  I am using the hashicorp/precise64 box, which includes Ubuntu.  I am also specificying the relative path to my boostrap.sh file, which will be used to provision the machine, and I am forwarding my ports, which wasn’t really neccisary but whatever.

Most of the complexity for my virtual machine is in the boostrap.sh file.  This was significantly more work than I anticipated, but seems really simple in retrospect.  Surprisingly, the biggest challenge that I faced was installing Java to meet the needs of my use case.  Not only does my boostrap.sh file provision my machine with all of the software that it needs, it also automatically runs my test code.  Pretty neat!

#!/usr/bin/env bash

sudo apt-get --assume-yes update
sudo apt-get --assume-yes install curl
sudo apt-get --assume-yes install unzip
sudo apt-get --assume-yes install xvfb
sudo apt-get --assume-yes install -y firefox
sudo apt-get --assume-yes install -y default-jre
sudo apt-get --assume-yes install python-software-properties
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get --assume-yes update
echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | sudo /usr/bin/debconf-set-selections
sudo apt-get --assume-yes install -y oracle-java8-installer
sudo apt-get --assume-yes install -y oracle-java8-set-default
sudo update-java-alternatives --set java-8-oracle
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo Xvfb :99 -ac &
sudo export DISPLAY=:99 &
mkdir webdriverio-test && cd webdriverio-test
curl -O http://selenium-release.storage.googleapis.com/3.0/selenium-server-standalone-3.0.1.jar
curl -L https://github.com/mozilla/geckodriver/releases/download/v0.11.1/geckodriver-v0.11.1-linux64.tar.gz | tar xz
xvfb-run -a java -jar -Dwebdriver.geckodriver.driver=./geckodriver selenium-server-standalone-3.0.1.jar > ./log.txt &
npm install webdriverio
curl -O https://raw.githubusercontent.com/jcreager/webdriverio-getting-started/master/test.js
node test.js

The bootrap.sh file is simply a list of bash commands to execute, listed in the appropriate order.  It may have made sense to edit the file system permissions rather than using sudo for a number of commands.  Vagrant can simplify that by allowing you to declare the commands one time in the bootstrap.sh file.

After setting up my Vagrantfile and boostrap.sh file, I was ready to test again by running the vagrant up command.

vagrant up
...
==> default: title was: Google

Success!  I removed some key commands to create a counter example to my test code, which demonstrates what happens if you do not set certain dependencies such as installing geckodriver, and declaring the path to its executable.  It is included as a subdirectory of the main repository.  If you want to see it in action, vagrant up in that directory to watch the code execute and reproduce the error that I encountered before.  You will need to vagrant ssh into the machine and access the log.txt in /vagrant to see the error logs from Selenium Standalone, because I am redirecting the output of Selenium Standalone to that file.

The best part of all of this is that I was able to share my results with the maintainers of webdriverio along side my pull request to update their guide to make validating the proposed changes easy.

Happy  automating!