Erik Simmler

Internaut, software developer and irregular rambler

Building consistent virtual machines with Packer and Vagrant

Iā€™ve been using Vagrant to manage local development environments for a while, but there are subtle differences between the base Ubuntu Vagrant box and the Ubuntu Cloud Images I typically use in production. Since the base set of packages donā€™t match exactly, builds would occasionally fail on AWS after working fine locally. While I always caught these issues before they went live, the mismatch was annoying at best.

While looking to simplify a server image creation scheme, I saw an opportunity to standardize the base VMs across environments. My main goal was to replace a bespoke Amazon Machine Image (AMI) creation script with Packer, which not too coincidentally ties in quite well with Vagrant.

The idea was to use a nearly identical base machine as a starting point both locally and on AWS. The first step is to get an Ubuntu Cloud Image suitable for use in Packerā€™s Virtualbox builder. When passed the name of the Ubuntu release you wish to use (e.g. $ dlbox.sh trusty), the following script will unzip the .ovf/.vmdk files into an folder named ubuntu.$RELEASE.box.

#!/usr/bin/env bash
set -eu

BOX_URL=https://cloud-images.ubuntu.com/vagrant/$1/current/$1-server-cloudimg-amd64-vagrant-disk1.box
mkdir -p ubuntu.$1.box
curl $BOX_URL | tar -x -C ubuntu.$1.box

Next you need to identify a matching EC2 image with the AMI Locator. At this moment, the EBS equivalent is ami-e63b3e8e.

With that, we can fill out a rudimentary packer.conf. This will launch matching machines locally and on AWS. Provisioning steps are very situation dependent, so I left them out.

{
  "variables": {
    "release_id": null,

    "source_ovf": "ubuntu.trusty.box/box.ovf",
    "source_ami": "ami-64e27e0c",

    "vagrant_private_key": "{{ env `HOME` }}/.vagrant.d/insecure_private_key"
  },
  "builders": [
    {
      "name": "vagrant",
      "type": "virtualbox-ovf",
      "source_path": "{{ user `source_ovf` }}",
      "guest_additions_mode": "disable",
      "ssh_username": "vagrant",
      "ssh_key_path": "{{ user `vagrant_private_key` }}",
      "ssh_wait_timeout": "30s",
      "shutdown_command": "echo 'packer' | sudo -S shutdown -P now"
    },
    {
      "name": "aws",
      "source_ami": "{{ user `source_ami` }}",
      "ami_name": "aws.{{user `release_id`}}.{{isotime \"2006-01-02.0304\"}}",
      "type": "amazon-ebs",
      "region": "us-east-1",
      "instance_type": "t1.micro",
      "ssh_username": "ubuntu"
    }
  ],
  "post-processors": [
    [
      {
        "type": "vagrant",
        "only": ["vagrant"],
        "output": "web.{{user `release_id`}}.box"
      }
    ]
  ]
}

You can use the above config with this command (omit the -only flag to enable the AWS builder).

$ packer build -var 'release_id=foo' -only=vagrant packer.json

Obviously there are many customizations you can make to suit your situation. I actually feed a previously built image into packer as the source Box/AMI (just change/override the source_* variables). This enables a configuration management system such as Salt, Ansible or Chef to update the images incrementally, potentially saving a ton of time.

This isnā€™t an exhaustive tutorial on Packer, which has excellent documentation and a helpful community.