Vagrant and Ansible for Dev Machines

With my new job came, of course, lots of new projects. They cover quite a wide range of system requirements and so I've been creating ansible-provisioned vagrant machines for each one to make it easy to set up on other platforms. I thought I'd share some examples of my setup, in case anyone is interested, but more importantly so I can swiftly look this up when I start the next new project!

Start with the Vagrantfile

Vagrant is configured with a Vagrantfile. Here I'm setting the simplest options - which box to use, a name for the box, any directories I want to mount, and then the instruction to use ansible for the rest. Here's that basic config:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"
  config.vm.network :private_network, ip: "192.168.121.8"
  config.vm.synced_folder '.', '/vagrant'

  config.vm.provider "virtualbox" do |vb|
      vb.name = "example-app"
  end

  #
  # Run Ansible from the Vagrant Host
  #
  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "playbook.yml"
    ansible.host_vars = {
        "default" => {
            "ansible_python_interpreter" => "/usr/bin/python2.7"
        }
    }
  end

end

This will bring up a basic Ubuntu 16.04 box (my preferred distro although it shipped with Python 3 rather than 2 which is why I need the ansible_python_interpreter setting and why I need to set the box name as by default the early versions of this box had a hardcoded name which broke things when you used it as the basis of more than one VM) and then hand off to ansible to do everything else.

Ansible To Make Technology Dance

There are a few different tools that would fit here but I've picked ansible for my VMs and so far I have found it quite approachable. In particular its declarative approach makes it easy to iteratively build up a machine by adding to the ansible recipe and reprovisioning repeatedly rather than always having to destroy the machine and start completely from scratch to test changes.

In the Vagrantfile example above, we set the provisioner to be ansible and give the playbook parameter as playbook.yml. The playbook is the recipe for cooking up the virtual machine you desire: it can be used to install packages, restart services, run commands, or really anything else we need to do to get the platform we need.

Rather than using the commands we already know, and typing them all in turn though, ansible has neat wrappers for almost all the tasks we could want to do. You'll probably want to refer to the excellent documentation for more information on the various modules that are in these examples and for others you want to use yourself.

Here's a quick cheatsheet of some of the patterns from my playbook.yml:

First-few-lines boilerplate

---
- hosts: all
  gather_facts: no
  tasks:
    - name: apt-get update 
      raw: sudo apt-get update -qq 
    - name: Install python 2.7 
      raw: sudo apt-get install -qq python2.7 

    - name: Fix hostname to allow sudo
      remote_user: root
      become: yes
      lineinfile: dest=/etc/hosts line='127.0.0.1 ubuntu-xenial' owner=root group=root mode=0644

Some of this is specific to the box that I had some issues with - since Python 2.7 isn't installed by default, currently ansible requires this older version of Python and so I found I needed to disable the fact-gathering and then install the required python to get the process started. The second block teaches the vagrant box its name which prevents an annoying warning when sudoing things.

Install an apt package

    - name: Install git
      remote_user: root
      become: yes
      apt: pkg=git

The syntax for setting which user to use changed between versions of ansible, this is the currently-correct syntax but you'll see lots of examples using the older ones too so if you're copying/pasting examples and having issues, check this. Also if my examples aren't working, check the comments for errata and updates - and if you don't find what you're looking for, add a comment yourself!

When installing apt packages, you can also give a comma-separated list:

    - name: Install Node + npm
      remote_user: root
      become: yes
      apt: pkg=nodejs,npm,nodejs-legacy

Running arbitrary shell commands

We can use the shell module to run commands:

    - name: Install node worker dependencies
      remote_user: root
      become: yes
      shell: npm install
      args:
          chdir: /vagrant/src/worker/

For many tasks, there are ansible wrappers (check out the files modules for example) already available which are recommended and stop the command from being blindly repeated on every provision.

For example the following block is from a project where I'm compiling couchdb from scratch:

    - name: Compile CouchDB
      remote_user: root
      become: yes
      shell: ./configure --disable-docs && make
      args:
        chdir: /vagrant/couchdb
        creates: bin/couchjs

By using the creates property, I can tell ansible how to tell if this tool has already been compiled - it stops it from repeating the compile every time.

Technology-specific package managers

There are wrappers for all the dependency managers as well, here are two examples where I'm installing dependencies for npm and for pip (because polyglot is cool!)

    - name: Install Grunt
      remote_user: root
      become: yes
      npm: name=grunt-cli global=yes

    - name: Install Requestbin dependencies
      pip: requirements=/vagrant/requestbin/requirements.txt

Working with services

If you change configuration and need to restart your webserver or any other service, there's a command for that too

    - name: Restart RabbitMQ
      remote_user: root
      become: yes
      service: name=rabbitmq-server state=restarted

These examples are my reference library for my next project - if they're helpful to you too, then I'm delighted :) Comments are welcome!

4 thoughts on “Vagrant and Ansible for Dev Machines

  1. You can automatically run [code]apt-get update[/code] as part of the Node installation if you specify two extra parameters to the [code]apt[/code] module:

    [code]
    update_cache: yes
    cache_valid_time: 86400
    [/code]

    If the cache is older than [code]cache_valid_time[/code] seconds, Ansible will automatically fetch the latest package information before installing the packages. It's also a bit nicer than running [code]apt-get update[/code] as a raw command because if you run the playbook multiple times, the package cache will only be updated once. I think you will need to omit [code]gather_facts[/code] though in order for it to work.

  2. Hi Lorna Jane,

    Thanks for this excellent post. I use Ansible for managing host configuration in production. Today, I was just about to set out on some fresh development for which I wanted to use Vagrant to spin up an Ubuntu VM on my MacBookPro. But I was stuck because I did not want one set of box build config ( VagrantFile ) for dev and then another set ( Ansible ) for Prod. What you've shared here is a perfect match.

    ( when Google first showed my LornaJane website, I thought, "do those Yoga people code ??" :)

    Thanks for sharing.

    • PS: I would suggest replacing 'ansible' provisioner with 'ansible_local' . Then there's not dependency that ansible is installed on the Vagrant Host machine. Vagrant will automatically install it on the Guest you are creating.

Leave a Reply

Please use [code] and [/code] around any source code you wish to share.