Setting up a Local Development Environment with Vagrant and Puppet
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 runningvagrant halt
thenvagrant 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 withvagrant 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.