Finished files available here.

Lately, I have been finding a lot of the documentation on these tools to steer towards enterprise use cases such as managing and deploying multiple machines; so I decided to document a single-user local development setup.

This example will setup the latest Ruby on Rails running on Ubuntu Server 14.04. The parent OS used is OS X.

The purpose of this example is to try to be as explicit as possible to explain how these tools work without going too far in scope.

##Prerequisites

On the parent OS, install Vagrant and VirtualBox.

##Virtual Machine Base box

Vagrant uses Ruby, though the package includes an embedded Ruby interpreter.

Vagrant works by starting with a ‘base box’ such as Ubuntu, Debian, Windows… that is then built on top of. You can create your own base boxes, but this example will start with a pre-existing box.

You can find base boxes at Vagrantbox.es and Vagrant Cloud.

Using a box from Vagrant Cloud, import Ubuntu Server 14.04 via the command line vagrant box add ubuntu/trusty64. Vagrant stores base boxes at ~/.vagrant.d/boxes, not in the directory you run the command.

Navigate to a directory where you want this project and all its files to reside.

Initialise a new Vagrant project vagrant init ubuntu/trusty64.

Vagrant will create a Vagrantfile file in the directory. This is a configuration file for the VM. The # character in a Vagrantfile is a comment. If you open the Vagrantfile you will see the lots of helpful settings commented out.

At this stage you can boot up the virtual machine using vagrant up and once it is booted you can shell into the machine using vagrant ssh (you should not have to enter a password because when base boxes are created the usual configuration is for the root password, main account username, and main account password to all be vagrant).

Leave the machines' shell with exit and then shutdown the machine using vagrant halt.

##Provisioning

###Introduction

Vagrant uses Provisioners to setup the virtual machine performing tasks such as installing packages, starting services, creating users. As well as provisioners like Puppet and Chef you can just use shell scripts and batch scripts.

When working on provisioning a Vagrant box the following commands are useful:

  • vagrant reload --provision equivalent of running vagrant halt then vagrant up and running the provision files again (will not do it by default)
  • vagrant destroy if you mess up the base box with incorrect provisioning destroy the machine and then start again with vagrant up.

When you have finally finished setting up an environment you can then just run vagrant up and vagrant halt each time and not have to keep re-doing the provisioning. Vagrant should only run provisioning if there are differences compared to the last time it ran.

The provisioner we will use is Puppet. Puppet comes installed on the base box we started with.

###Directory Structure

In puppet manifest files (.pp) contain Puppet code.

Create a directory structure that looks like this:

|---provision
|	|
|	|---manifests
|	|	|
|	|	|---site.pp
|	|	
|	|---modules
|	|	|
|	|	|
|	|	|---ruby
|	|	|	|	
|	|	|	|---manifests
|	|	|		|
|	|	|		|---init.pp
|	|	|
|	|	|---rails
|	|	|	|
|	|	|	|---manifests
|	|	|		|
|	|	|		|---init.pp
|	|	|
|	|	|
|---Vagrantfile

site.pp will be our global provision file, in some documentation, you see this file named default.pp. The rest of the required code will be split into separate modules. Each module is a class - we will come onto this later.

Puppet has expectations on module directory structure and so adhering to this is important.

Open the Vagrantfile created previously and find this part: #config.vm.provision "puppet" do |puppet|. Add:

config.vm.provision "puppet" do |puppet|
	puppet.manifests_path = "provision/manifests"
	puppet.manifest_file  = "site.pp"
	puppet.module_path = "provision/modules"
end

This will match the directory structure set up previously.

Modules

Open ruby/manifests/init.pp.

Puppet uses resource types for each step. These perform different functions such as installing software packages, running commands, creating users, copying files… A full explanation of these is out of the scope of this article. You can find one here: docs.puppetlabs.com/puppet/latest/reference/lang_visual_index.html

Puppet expects the class name to match the directory name, in this case, ruby.

class ruby {
	
}

The first steps are to add a third-party Ubuntu package repository with a package for Ruby 2.1 and then update the package manager for the addition to take effect. This requires exec(command) resources:

class ruby {
	exec { "Add ruby2.1 repository":
		command 	=> "sudo apt-add-repository ppa:brightbox/ruby-ng",
		path 		=> "/usr/bin/"
	}

	exec { "Update package manager":
		command 	=> "sudo apt-get update",
		path 		=> "/usr/bin/",
		timeout 	=> 0,
		require 	=> Exec["Add ruby2.1 repository"]
	}
}

The command, and path attributes are probably explanatory. The timeout attribute is useful for commands that may take a long time to run and the require attribute is how you declare dependencies in Puppet. Note that when using require the resource type should be capitalised.

After that install the two packages needed with the package resource:

class ruby {
	exec { "Add ruby2.1 repository":
		command 	=> "sudo apt-add-repository ppa:brightbox/ruby-ng",
		path 		=> "/usr/bin/"
	}

	exec { "Update package manager":
		command 	=> "sudo apt-get update",
		path 		=> "/usr/bin/",
		timeout 	=> 0,
		require 	=> Exec["Add ruby2.1 repository"]
	}

	package { "ruby2.1": 
		ensure 		=> "installed",
		require 	=> Exec["Update package manager"]
	}

	package { "ruby2.1-dev": 
		ensure 		=> "installed",
		require 	=> Package["ruby2.1"]
	}
}

For the rails module declare that Ruby needs to be installed first using the require function (if you have setup your modules directories correctly Puppet will automatically know where to find it).

class rails {
	# prerequisites
	require ruby
}

Then install the package dependencies for Rails and the rails gem.

class rails {
	# prerequisites
	require ruby

	$dependencies = ["apache2", "curl", "git", "libmysqlclient-dev", "libsqlite3-dev", "mysql-server", "nodejs"]

	package { $dependencies:
    	ensure   => 'installed'
	}

	package { 'rails':
    	ensure   => 'installed',
    	provider => 'gem'
	}
}

To indicate that rails is a gem and should be installed from RubyGems the provider attribute is used.

Site.pp

Because the modules have been structured as Puppet expects, Puppet will automatically include these in the site.pp manifest. You could be explicit with:

import "ruby"
import "rails"

But this is not needed.

To run what we have created we just need to run the rails module because the other ruby module has been declared as a dependency.

You can test all this by running vagrant reload --provision.

After it has completed installation, shell into the box vagrant ssh and check versions with ruby -v and rails -v. Run rails new demo to generate a project.

Shared Directories

To keep this box as a machine that can be destroyed easily we will host our application code in a directory in the parent OS and map the folder to a location inside the VM.

Vagrant maps one directory by default: the directory containing the Vagrantfile in the parent OS is mapped to /vagrant in the box.

To map other directories search for config.vm.synced_folder in the Vagrantfile.

If you were working on an already existing code base, at this point you would be putting it into the directory in the parent OS. However as we are initialising a new project use config.vm.synced_folder "project", "/home/vagrant/myproject/".

Create this directory in the parent OS.

|---provision
|---Vagrantfile
|---myproject

Reload the VM and ssh into the box. In /home/vagrant/ run rails new myproject. This shared directory should now contain the Rails project code.

Port Forwarding

Lastly, we need to be able to see the application running. Halt the machine and go back to the Vagrantfile.

Rails by default runs on port 3000. Add this line config.vm.network "forwarded_port", guest: 3000, host: 3030.

Boot up the VM and naviagate to /myproject/. Run rails server, then in the parent OS visit http://localhost:3030 to see the Rails new application screen.