6 min read

Vagrant and Ansible for Local Development

If you’re using Ansible to provision/manage your infrastructure it only makes sense to utilize the same setup locally instead of having to go through all of the same configuration again. It eliminates a bunch of the “works on my machine” issues, it documents the entire process, makes it easier for new developers to get up to speed, etc.

I’m going to setup the open-source Ruby on Rails application Lobste.rs as my application.

For the setup that I’ve been utilizing, everything but the physical files will be in Vagrant. Well, they’re in there but just as a mount within the virtual machine. I find that this makes it an easier transition for utilizing your existing favorite editor. It’s also very helpful if you’re developing multiple applications and you’ve already configured them locally and aren’t ready to work on them via Vagrant. It’s more of an opt-in approach to developing applications when you’ve already setup applications locally which is preferred instead of switching everything over to Vagrant immediately.

First we’ll clone the Lobste.rs repository to our machine:

git clone git@github.com:jcs/lobsters.git

Install Homebrew, Ansible(via homebrew works well) and Download Vagrant via their recommended instructions.

Now we need to install Virtualbox, I prefer to use brew cask for this:

brew install caskroom/cask/brew-cask
brew cask install virtualbox

Now we can start working on the Ansible playbook for provisioning the host machine. It’s just going to be a simple setup that involves resolving *.dev to localhost with dnsmasq. If you already have *.dev resolving to your local machine you could always skip this portion, it’s the steps from this blog post wrapped up into a simple playbook.

Create a new directory for Ansible alongside the Lobste.rs directory:

mkdir ansible && cd ansible

Create the osx.yml playbook in the ansible directory with the following contents:

---
- hosts: all
  tasks:
    - name: Install homebrew dependencies
      homebrew: name=dnsmasq

    - name: Add *.dev resolver
      lineinfile:
        state: present
        line: nameserver 127.0.0.1
        create: yes
        dest: /etc/resolver/dev
        owner: root
        group: wheel

    - name: Resolve *.dev to localhost
      lineinfile:
        state: present
        line: address=/.dev/127.0.0.1
        create: yes
        dest: /usr/local/etc/dnsmasq.conf

We’ll run this playbook prior to running our regular provisioner. It’s a simple playbook that utilizes dnsmasq to map *.dev to localhost so that you can have zomgansibleisawesome.anything.dev as a resolvable domain.

Since we’ll be running this playbook against our host machine, we need to create a simple inventory file for it. Create the inventory file with:

[localhost]
localhost           ansible_connection=local

We just need to run this osx.yml playbook once before we provision our Vagrant machine so that it sets up our local resolver. Go ahead and run the playbook with:

ansible-playbook -i inventory -s -K osx.yml

Next we’ll create the Vagrantfile, this file tells Virtualbox how to provision the virtual machine. Create a Vagrantfile with the following contents:

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

# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "ubuntu/trusty64"

  config.vm.network "private_network", ip: "192.168.50.4"

  config.vm.define "rails" do |rails|
    rails.vm.synced_folder "~/src", "/home/vagrant/src"

    rails.vm.provision "ansible" do |ansible|
      ansible.playbook = "rails.yml"
      ansible.host_key_checking = false
      ansible.extra_vars = { ansible_ssh_user: 'vagrant' }
      ansible.sudo = true
    end
  end
end

The Vagrantfile assumes that Lobsters sits in ~/src/lobsters, modify as needed. With Vagrant you can have multiple machines defined in one Vagrantfile, here we’re creating the “rails” machine. Later on you could add a “db” machine and a “search” machine to simulate a full stack locally.

Now we can get started on building our rails.yml playbook that our Vagrantfile will be using. We’re going to utilize the abtris.nginx-passenger and zzet.rbenv roles to get us going:

ansible-galaxy install abtris.nginx-passenger zzet.rbenv -p roles/

Open up rails.yml and now we can get started on getting our rails stack configured. I’ve added the base set of variables needed for zzet.rbenv as in playbook variables as well.


---
- hosts: all
  vars:
    rbenv:
      env: user
      version: v0.4.0
      ruby_version: 1.9.3-p429
    rbenv_users: ['vagrant']
  roles:
    - abtris.nginx-passenger
    - zzet.rbenv
    - nginx

THE NGINX ROLE

Now we’ll setup our virtualhost with nginx and configure passenger, first we’ll make the main task of our nginx role.

roles/nginx/tasks/main.yml:


---

- name: Read passenger configuration
  register: passenger_root
  command: passenger-config --root
  changed_when: false

- name: Add passenger configuration
  template:
    src: passenger.conf.j2
    dest: /etc/nginx/conf.d/passenger.conf
  notify: nginx reload

- name: Add virtualhost configuration
  template:
    src: virtualhost.conf.j2
    dest: /etc/nginx/sites-available/{{ item.server_name }}
  with_items: virtual_hosts
  notify: nginx reload

- name: Disable virtualhosts
  file:
    path: /etc/nginx/sites-enabled/{{ item.server_name }}
    state: absent
  when: item.disabled is defined
  with_items: virtual_hosts
  notify: nginx reload

- name: Enable virtualhosts
  file:
    src: /etc/nginx/sites-available/{{ item.server_name }}
    dest: /etc/nginx/sites-enabled/{{ item.server_name }}
    state: link
  when: item.disabled is not defined
  with_items: virtual_hosts
  notify: nginx reload

Add the mentioned templates:

roles/nginx/templates/virtualhost.conf.j2:



# {{ ansible_managed }}
server {
  listen 80;
  rails_env {{ item.rails_env }};
  passenger_enabled on;
  server_name {{ item.server_name }};
  root {{ item.root }}/public;
}

roles/nginx/templates/passenger.conf.j2:


# {{ ansible_managed }}
passenger_root {{ passenger_root.stdout }};
passenger_ruby /home/vagrant/.rbenv/shims/ruby;

We also need to define the virtual hosts for Nginx which we utilize to create the associated sites-* files in the main nginx task.

---
- hosts: all
  vars:
    rbenv:
      env: user
      version: v0.4.0
      ruby_version: 1.9.3-p429
    rbenv_users: ['vagrant']
    virtual_hosts:
      - server_name: lobsters.dev
        root: /home/vagrant/src/lobsters
        rails_env: development
  roles:
    - abtris.nginx-passenger
    - zzet.rbenv
    - nginx

Now that we have all of that configured, go ahead and run vagrant up which will add our new virtual host files and configure passenger to run with nginx. We’re almost finished, we still need to bootstrap the rails application within Vagrant.

SETTING UP OUR RAILS APPLICATION

To simplify setup edit the Gemfile in the Lobste.rs application and add dotenv-rails and remove sqlite3. Edit your .env file and add the following to it:

DATABASE_URL=mysql2://localhost/lobsters_development
SECRET_KEY_BASE=randomsecretsauce

I’m going to write a simple bin/setup script that will setup the lobsters application on the virtual machine. On the host machine create bin/setup with the following contents and make it executable:

bin/bundle 
bin/rake db:setup

Edit your Vagrantfile and add an additional provisioner within your rails vm but after the ansible provisioner:

rails.vm.provision "shell", inline: "cd ~/src/lobsters && bin/setup",
  privileged: false

We also need need to have bundler installed on the machine so that the bundle command will work properly to get the lobsters application setup. Edit your rails.yml file and add a task to install Bundler:


---
- hosts: all
  vars:
    rbenv:
      env: user
      version: v0.4.0
      ruby_version: 1.9.3-p429
    rbenv_users: ['vagrant']
    virtual_hosts:
      - server_name: lobsters.dev
        root: /home/vagrant/src/lobsters
        rails_env: development
  roles:
    - abtris.nginx-passenger
    - zzet.rbenv
    - nginx
  tasks:
    - name: Install gem dependencies
      apt: name={{ item }} state=present
      with_items:
        - mysql-server
        - libmysqlclient-dev
        - nodejs

    - name: Install bundler
      sudo_user: vagrant
      command: bash -lc "gem install bundler"

Now we can go ahead and reprovision our machine with:

vagrant provision

Once that’s complete we can map a hostname in /etc/hosts to the Vagrant virtual machine’s IP address:

192.168.50.4 lobsters.dev

Adding the entry to /etc/hosts allows you to opt-in to which applications you’d like to hit your virtual machine and which you’ll continue to develop in OSX.

Now you should be able to visit http://lobsters.dev on your local machine and see your application running.