Virtual Environments with Vagrant

Jared Carroll ·

After a recent run-in with an irate local sysadmin, I was convinced to try out developing on a virtual machine. His argument was that developing on one platform, such as OS X, when you are deploying to another, such as Linux, was just asking for trouble. Specifically he was referring to using Ruby gems with native extensions requiring C libraries that may not be installed on the deployment server or they are installed but the version is different than your local environment. While I haven’t ran into too many of those issues, I could see his point.

Similar problems can happen even when using the same platform. I recently had issues on a team related to some developers using brew vs MacPorts and their default install locations. If everyone was using the same package manager these problems could have been avoided. It was also taking basically a whole day to get a new developer’s environment up and running. Some automated provisioning tools could of really sped this process up.

Fortunately the Ruby community has an excellent gem that allows us to both create and provision virtual development environments: Vagrant. I decided to give it a try. After a few months of using it, I’m hooked.

Getting Started

To get a vm up and running use Vagrant’s excellent documentation for information related to Vagrantfiles and the Vagrant command line app. Vagrant calls its vm templates boxes. Vagrant Boxes is a great site that pulls together all the various boxes floating around online. I found no problems using their various Debian, Ubuntu, and CentOS boxes.

Provisioning a VM

Once you have a vm up and running you’ll want to automate provisioning it. Vagrant supports both Chef and Puppet for provisioning. I’ve had past experience with Chef and decided to use it exclusively. There are already Chef cookbooks for most of the usual tools you’ll need e.g. MySql, Postgresql, Apache, etc. Opscode’s (the company behind Chef) cookbooks repository on Github was my main resource. Vagrant includes support for both Chef Solo and Chef Server.

Chef Solo

I first decided to keep all the Chef cookbooks in the same repository as the app, storing them in the root of the codebase in a chef/ directory.

Here’s the provisioning configuration using Chef Solo:

Vagrant::Config.run do |config|
  ...
  config.vm.provision :chef_solo do |chef|
    chef.roles_path = 'chef/roles'
    chef.cookbooks_path = 'chef/cookbooks'
    chef.add_role 'my-app'
  end
end

I used Chef’s roles to simplify the provisioning configuration in the Vagrantfile. A role in Chef can reference recipes and other roles. To keep my configuration as short as possible I used one role named after the project. This role contained all the app’s dependencies.

name "my-app"
run_list("recipe[apt]", "recipe[ ruby ]", "recipe[mysql::server]")
override_attributes "mysql" => { "server_root_password" => "" }

This worked great because any changes I needed to make would be versioned along with the rest of the codebase. However, other developers on the team weren’t using Vagrant and didn’t like having the Chef infrastructure in the codebase. So I removed it from the repository and added the directory to my .gitignore. However, I still wanted the cookbooks on another machine for backup purposes. Chef Server was the answer.

Chef Server

Chef Server allows you to keep all your cookbooks on a separate server. During provisioning all the cookbooks will be downloaded and executed on the vm. This also allows you to reuse the same cookbooks across multiple vms.

Fortunately you don’t have to setup your own Chef server. Opscode provides a hosted Chef Server for free (free as in a maximum of 5 nodes where a node is equal to a vm).

Here’s the provisioning configuration using Chef Server:

Vagrant::Config.run do |config|
  ...
  config.vm.provision :chef_server do |chef|
    chef.chef_server_url = "https://api.opscode.com/organizations/carbon5"
    chef.validation_key_path = "~/.chef/carbon5-validator.pem"
    chef.validation_client_name = "carbon5-validator"
    chef.node_name = 'my-app'
    chef.add_role 'my-app'
  end
end

This is my current preferred way of provisioning. You keep the cookbooks out of the codebase but still get the benefit of having them stored remotely. I’ve amassed a collection of 20 or so cookbooks on my hosted Chef Server account. This really helps cut down the amount of time to provision an environment for a new codebase.

Performance

Performance seems to be a common issue for people when it comes to developing locally on a vm. Vagrant uses a shared folder between the host OS and the vm (guest OS). This allows you to check out the code once on your host OS and share this directory with the vm. I’ve found the peformance to be fine and not noticeably slow at all. For more info and benchmarks see the Vagrant documentation.

Workflow

I use tmux to run multiple terminals inside a single window. I usually split my window horizontally with an editor up top and a shell ssh’d into the vm on the bottom.

Since Vagrant uses a shared folder between the host OS and the vm I can have my editor installed and configured on my host OS and use it to edit code that will run on the vm. I’ll typically have a few more terminals ssh’d into the vm as well for various other tasks such as logging into the db or tailing a log file.

Benefits

Virtual environments give you the ability to develop in an environment identical to your deployment environment. This helps eliminate frustating “works on my machine” bugs and deployment related issues. Your sysadmin will thank you.

Vagrant’s provisioning support helps reduce the time spent onboarding a new developer onto a project. This also means no more maintaining a “Project Setup” page on a wiki because setup is a single command: vagrant up. This could be really useful for open source projects. Imagine cloning a repo, then running vagrant up to have a nice sand-boxed fully provisioned environment up and running.

For Ruby developers, a separate vm eliminates the need for RVM. RVM’s per-project gemsets are great for keeping gem dependencies separate but a separate vm takes this one step further to a per-project OS.

Working in a vm feels similar to working in a topic branch in Git. You have a completely separate environment that you can log into and out of at any time. Your host OS remains clean of any project specific dependencies. Using Vagrant makes working with virtual environments as easy as branching in Git. Once you try developing like this you’ll never want to go back.