Nick Hammond

Software Developer, Cyclist, & Traveler.

Packaging and sharing your Vagrant box

In ansible, development, vagrant

A while ago I wrote about simplifying your local development with Ansible and Vagrant, then I realized it’d probably be helpful to share your new virtual machine setup with other people. Sharing your prepackaged virtual machine is akin to a docker container but much less magic about what’s going on inside of that virtual machine.

Files still work like a normal server, networking, storage all of the additional layers that you have to deal with once you start getting into Docker and Docker compose. Docker is great, but sometimes a good old simple virtual machine will get the job done and get out of the way so you can get back to adding value to your applications.

With a typical full stack server there’s a decent amount of setup that needs to be figured out and put into place - installing packages, compiling languages, configuring applications, etc. While it’s useful to have people setting their development environment up locally this way not everyone should have to build the server from scratch. There should be a few users who know how to modify the built VM to be able to rebuild it and make upgrades but once it’s built that final snapshot should be shareable with whoever needs to work on the same application.

Once you package the full virtual machine up you get to the point where it’s similar to just sharing a docker container but without all of the docker overhead. If someone else needs to get that stack up and running it’s as simple as specifying the prebuilt virtual machine as a base box and booting the virtual machine up. Once the pre-built machine is up the end user can also make any modifications needed to it if they need or just start plugging away.

In order to utilize this approach though there are a few important pieces to take into account:

  • Ensuring none of your secrets or private files are on the virtual machine if you end up using your private key for Github clones for instance. This can be part of the end user’s setup instead.
  • Remove any snowflake behavior or customizations to ensure it’s more predictable for future builds.
  • Get the virtual machine to be as standard and consistent as possible that makes sense to not just you but whoever is going to utilize the container/VM.

Ideally at the end of this getting anyone else up and running with Lobste.rs is as simple as:

  1. Install Vagrant
  2. Install VirtualBox
  3. Create a new Vagrantfile with the base box specified as the prebuilt one
  4. Run vagrant up, no Ansible steps should run and the VM should be ready in under a minute

If we start where we left off in the other post of having a functioning local virtual machine in Virtualbox then the remaining steps are:

  1. Package up the vagrant virtual machine into a .box file
  2. Create a new Vagrantfile that references the packaged .box file as the base box
  3. Set an IP for the new virtual machine so that we can easily use /etc/hosts to point to the virtual machine

From the current working directory of your functioning virtual machine you can go ahead and package up that virtual machine with:

vagrant package --output lobsters-v0.1.box

Once this finishes compressing your files it’s going to leave you with a new lobsters-v0.1.box, awesome. The vagrant package command automatically shuts down your virtual machine, creates a snapshot of it, and compresses it to a reusable virtual machine file.

This .box file can now be utilized just like you would when you set:

config.vm.box = "ubuntu/bionic64"

Instead of your starting point being some other base image such as Ubuntu it’ll now be a ready to go version of your Lobsters application. In order to do that I’d recommend a separate Vagrantfile that you share that references this box and provides the necessary configuration to get the virtual machine up and running.

# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.env.enable # Enable vagrant-env(uses dotenv)

  config.vm.box = ENV['BOX'] || "lobsters-v0.1.box"

  # Define a VM for the local development environment, you can modify the memory & CPUs
  # utilized in .env.
  config.vm.define ENV['VM_NAME'] || 'lobsters', primary: true do |lobsters|
    rails.vm.provider "virtualbox" do |vb|
      vb.customize ["modifyvm", :id, "--cpuexecutioncap", ENV['CPU_MAX'] || "50"]
      vb.customize ["modifyvm", :id, "--uartmode1", "disconnected"]

      vb.memory = ENV['MEMORY']
      vb.cpus = ENV['CPUS'] || 2
    end

    # This is the IP that you'll set in /etc/hosts on your macOS to tell your machine 
    # to look at your VM instead of resolving to your macOS local.
    rails.vm.network "private_network", ip: ENV['IP'] || "192.168.50.8"

    # Copy over your private key for private git clones
    rails.vm.provision "file", source: ENV['PRIVATE_KEY'], destination: "~/.ssh/id_rsa"
  end
end

Here’s a starter Vagrantfile, it doesn’t take into account things like updating files in the virtual machine which you can do with synced_folders which deserves its own post.

I’ve added a few additional settings that get loaded from your .env file so that whoever you share it with can easily set the memory, CPU, virtual machine name, and the specific box. It’s helpful to pull these out into a .env file so that they don’t get checked into version control and if this Vagrantfile needs to be swapped they can, for the most part, replace it completely.

P.S. We should keep in touch, subscribe below.