Vagrant: a closer look

Previously we have talked about the benefits of using virtual machines for local development and how Vagrant and Ansible help you manage your VMs. In this post we will talk about the Vagrant part of our setup.

Here's the repository that we use https://github.com/Webscope/vagrant-ansible-drupal7. We recommend you download it to better follow the description. So let's begin.

Vagrant allows you to spin up and take down your VMs with a single command. All the instructions are stored in a text file called Vagrantfile. You will need to have a Vagrantfile for each of your VMsYou can generate it by running the following command:

vagrant init

Our Vagrantfile starts by checking if the vagrant-triggers plugin has been installed. This plugin comes in handy if we need to perform various actions when booting up or shutting down the VM.

if !Vagrant.has_plugin?("vagrant-triggers")
  puts "'vagrant-triggers' plugin is required"
  puts "This can be installed by running:"
  puts
  puts " vagrant plugin install vagrant-triggers"
  puts
  exit
end

The next step is defining a few variables that will be used throughout the file:

# Find the current vagrant directory.
vagrant_dir = File.expand_path(File.dirname(__FILE__))
provision_hosts_file = vagrant_dir + '/provision/host.ini'
 
# Include config from provision/settings.yml
require 'yaml'
vconfig = YAML::load_file(vagrant_dir + "/provision/settings.yml")
 
# Configuration
boxipaddress = vconfig['boxipaddress']
boxname = vconfig['boxname']

The file settings.yml is an important one, as it contains all the settings for your VM. This file doesn't exist in a repo as it is specific to each VM, so it needs to be copied from example.settings.yml, and the settings should be adjusted according to your needs before you start the VM. Vagrant will need the following variables from settings.yml:

---
  boxipaddress: "192.168.100.100"
  boxname: "vagrant"
  memory: 1024
  cpus: 2
  webserver_hostname: 'vagrant-multi.local'

Other variables are for Ansible provisioning.

Here are some of the most basic VM settings, which are quite self-explanatory:

# Configure virtual machine options.
config.vm.box = "hashicorp/precise64"
config.vm.hostname = boxname
 
config.vm.network :private_network, ip: boxipaddress
 
# Set *Vagrant* VM name
config.vm.define boxname do |boxname|
end
 
# Configure virtual machine setup.
config.vm.provider :virtualbox do |v|
  v.customize ["modifyvm", :id, "--memory", vconfig["memory"]]
  v.customize ["modifyvm", :id, "--cpus", vconfig["cpus"]]
end

In order for the VM to use the DNS settings of your network adapter:

v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]

Another handy setting allows you to use your local SSH keys when connecting to remote servers from within the VM:

config.ssh.forward_agent = true

With Vagrant you can sync folders between your local computer and your VM. It's super cool, because you can edit code in your editor of choice, and the changes will be instantly synced into the VM, where your project actually lives. We are using NFS which is natively supported by Vagrant, it gives us far greater speeds compared to the default shared folders implementation. There's still one downside to NFS - it slows down when the number of files in the project becomes too big. It is the case for all Drupal projects, and we see the speed downgrade when running git commands from within the box, but there's no noticeable impact on syncing when files are being updated during development. NFS support is enabled as follows:

config.vm.synced_folder "./webroot",
  "/home/vagrant/sites/" + vconfig["webserver_hostname"],
  type: "nfs",
  create: true

Windows users might want to check out this plugin https://github.com/GM-Alex/vagrant-winnfsd

Now back to vagrant triggers. Every time when we run vagrant up, Vagrant launches a small helper Ansible playbook, which performs some pre-provisioning tasks. File host.ini is being created at this point and is populated with some helper variables, this file also serves as an indication that the VM has been booted.

# Run an Ansible playbook on setting the box up
if !File.exist?(provision_hosts_file)
  config.trigger.before :up, :stdout => true, :force => true do
    run 'ansible-playbook -i ' + boxipaddress + ', --ask-sudo-pass ' + vagrant_dir + '/provision/playbooks/local_up.yml --extra-vars "local_ip_address=' + boxipaddress + '"'
  end
end

When the VM is shut down or destroyed, another helper playbook executes a few cleanup tasks and it removes the host.ini file:

# Run the halt/destroy playbook upon halting or destroying the box
if File.exist?(provision_hosts_file)
  config.trigger.before [:halt, :destroy], :stdout => true, :force => true do
    run 'ansible-playbook -i ' + boxipaddress + ', --ask-sudo-pass ' + vagrant_dir + '/provision/playbooks/local_halt_destroy.yml'
  end
end

After the VM has been booted Vagrant launches the provisioning script:

config.vm.provision "ansible" do |ansible|
  ansible.playbook = vagrant_dir + "/provision/playbooks/site.yml"
  ansible.host_key_checking = false
  ansible.extra_vars = {user:"vagrant"}
  if vconfig['ansible_verbosity'] != ''
    ansible.verbose = vconfig['ansible_verbosity']
  end
end

Our VM is set up to host multiple sites, so our Vagrantfile also sets up synced folders and virtual hosts for every project. That happens after the box has been booted up and provisioned.

vconfig['vhosts'].each do |vhost|
  site_alias = vhost['alias']
  folder_path = vhost['path']
  config.vm.synced_folder folder_path,
    "/home/vagrant/sites/" + site_alias,
    create: true,
    type: "nfs",
    mount_options: ['rw', 'fsc'],  # the fsc is for cachedfilesd
    :nfs => nfs_setting
end
 
# Create all virtual hosts
config.trigger.after [:up, :reload], :stdout => true, :force => true do
  run 'ansible-playbook -i ' + boxipaddress + ', -K --user=vagrant --private-key=~/.vagrant.d/insecure_private_key ' + vagrant_dir + '/provision/playbooks/virtual_hosts.yml --extra-vars "local_ip_address=' + boxipaddress + '"'
end

Vagrant box can be accessed via SSH by running:

vagrant ssh

In order to shut down the VM, just run:

vagrant halt

To destroy the VM completely:

vagrant destroy

And that's basically it. For more information, please check the official documentation at https://docs.vagrantup.com/v2/.

We'll take a closer look at Ansible in the next post. Stay tuned.

You can also check out:

More blog posts by Michael Kudenko

Ansible is a great tool for automating tasks executed either locally or remotely. Today we will talk about using Ansible in conjunction with Vagrant for setting up a local development environment.
When you're developing a system that is supposed to work with multiple content types (or any other data structures), polymorphism can be quite helpful. Today we'll talk about how to organise your database schema and use Eloquent to implement polymorphic relations between your models.
Here at Webscope we pay a great deal of attention to our development process. We are working hard to make it as smooth and efficient as possible. We find that developing locally, as opposed to remotely, provides greater benefits. In this post we'll talk about streamlining our local development with Vagrant and Ansible.