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!