Scripting a Ruby on Rails Setup Using Vagrant and Puppet Librarian

Why would you want to put in extra work to make your development box replicable? There are lots of benefits! And I’ll give you the main motivators here:

Now that I’ve gotten your attention…I’d like to justify my use of Puppet Librarian, since you can script a deployment with just Puppet. I like to think of Puppet Librarian’s use like that of a Gemfile. Puppet allows you to write you own modules, but just like in the Ruby community with its gems, the Puppet community has written its own modules that can be used for setting up your machine. Puppet librarian produces a Puppetfile (and Puppetfile.lock) where you can write down which modules to import and their versions. Once they’re installed, you’ll notice through the Puppetfile.lock that even the modules’ dependencies have been installed as well. This not only ensures that every body is using the same versions of the modules, but also discourages anybody from modifying the modules.

Let’s go ahead and jump in to setting up your Ruby on Rails application on a virtual machine using a script:

  1. Make sure you have Vagrant installed.
  2. Make sure you have VirtualBox installed.
  3. Go to your app’s directory in your terminal.
  4. As per the vagrant website instructions, run vagrant init hashicorp/precise321
  5. Run vagrant up, and now you should have an ubuntu machine up and running.
  6. Run vagrant plugin install vagrant-librarian-puppet to install Puppet Librarian.
  7. Make the modules path: mkdir modules
  8. Make the puppet path: mkdir puppet
  9. Make the puppet manifests path mkdir puppet/manifests
  10. Create the init.pp file that will hold your puppet script: touch puppet/manifests/init.pp
  11. Create the puppet installation script file: touch puppet/bootstrap.sh
  12. Add a puppet installation script. I use the one I found here: . Copy it into puppet/bootstrap.sh. Make some minor changes to it so that your file looks like this:

    #!/usr/bin/env bash
    #
    # This bootstraps Puppet on Ubuntu 12.04 LTS.
    #
    set -e
    
    # Load up the release information
    . /etc/lsb-release
    
    DISTRIB_CODENAME="precise"
    REPO_DEB_URL="http://apt.puppetlabs.com/puppetlabs-release-${DISTRIB_CODENAME}.deb"
    
    #--------------------------------------------------------------------
    # NO TUNABLES BELOW THIS POINT
    #--------------------------------------------------------------------
    if [ "$(id -u)" != "0" ]; then
      echo "This script must be run as root." >&2
      exit 1
    fi
    
    if which puppet > /dev/null 2>&1 -a apt-cache policy | grep --quiet apt.puppetlabs.com; then
      echo "Puppet is already installed."
      exit 0
    fi
    
    # Do the initial apt-get update
    echo "Initial apt-get update..."
    apt-get update >/dev/null
    
    # Install wget if we have to (some older Ubuntu versions)
    echo "Installing wget..."
    apt-get install -y wget >/dev/null
    
    # Install the PuppetLabs repo
    echo "Configuring PuppetLabs repo..."
    repo_deb_path=$(mktemp)
    wget --output-document="${repo_deb_path}" "${REPO_DEB_URL}" 2>/dev/null
    dpkg -i "${repo_deb_path}" >/dev/null
    apt-get update >/dev/null
    
    # Install Puppet
    echo "Installing Puppet..."
    DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install puppet >/dev/null
    
    echo "Puppet installed!"
    
  13. Replace the Vagrantfile with the following:

    # -*- 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-server-12042-x64-vbox4210"
      config.vm.box_url = "http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210.box"
    
      config.vm.network :forwarded_port, guest: 3000, host: 3000
    
      config.vm.provision :shell, :path => "puppet/bootstrap.sh"
    
      config.vm.provision :puppet do |puppet|
        puppet.manifests_path = "puppet/manifests"
        puppet.manifest_file  = "init.pp"
        puppet.module_path = "modules"
      end
    end
    
  14. Add the puppet-librarian gem to your app’s Gemfile, and run bundle install locally. This should create the Puppetfile.

  15. Replace the Puppetfile with the following, adding rbenv and postgresql modules from Puppet Forge (assuming you’re using PostgreSQL as your app’s database):

    #!/usr/bin/env ruby
    
    forge "https://forgeapi.puppetlabs.com"
    
    mod "jdowning/rbenv", "1.3.0"
    mod "puppetlabs/postgresql", "4.0.0"
    
  16. Run librarian-puppet install --no-use-v1-api. This will create the Puppefile.lock file. This is where the module versions and their dependencies are set, just like the ruby-equivalent of the Gemfile.lock. We’re also telling it to use the latest Puppet Forge api. You should also now see that your modules directory should have the rbenv and postgresql directories.

  17. Now let’s add our own script to make the necessary installations and configurations to the ubuntu box. Add the following to puppet/manifests/init.pp, modifying your database names and passwords as needed:

    Exec { path => "/home/vagrant/bin:/usr/local/rbenv/bin:/usr/local/rbenv/shims::/usr/local/rbenv/shims/bin:/usr/bin:/bin:/usr/sbin:/sbin" }
    
    exec { "apt-update":
      command => "/usr/bin/apt-get update",
      onlyif => "/bin/bash -c 'exit $(( $(( $(date +%s) - $(stat -c %Y /var/lib/apt/lists/$( ls /var/lib/apt/lists/ -tr1|tail -1 )) )) <= 604800 ))'"
    }
    
    Exec["apt-update"] -> Package <| |>
    
    package { "libpq-dev": ensure => present }
    package { "nodejs": ensure => present }
    
    # --- Postgres -----------------------------------------------------------------
    class { "postgresql::server":
      postgres_password => "password"
    }
    
    postgresql::server::role { "postgres":
      password_hash => postgresql_password("postgres", "password"),
      superuser => true,
      require => Class["postgresql::server"]
    }
    
    postgresql::server::db { "blog_development":
      user     => "postgres",
      password => postgresql_password("postgres", "password"),
      require => Postgresql::Server::Role["postgres"]
    }
    
    postgresql::server::db { "blog_test":
      user     => "postgres",
      password => postgresql_password("postgres", "password"),
      require => Postgresql::Server::Role["postgres"]
    }
    
    # --- Ruby ---------------------------------------------------------------------
    class { "rbenv": }
    
    rbenv::plugin { "sstephenson/ruby-build": }
    rbenv::plugin { "ianheggie/rbenv-binstubs": }
    rbenv::plugin { "sstephenson/rbenv-gem-rehash": }
    
    rbenv::build { "2.1.1":
      global => true,
      require => [Rbenv::Plugin["sstephenson/ruby-build"], Rbenv::Plugin["ianheggie/rbenv-binstubs"], Rbenv::Plugin["sstephenson/rbenv-gem-rehash"]]
    }
    
    file { "/home/vagrant/bin":
      ensure => directory,
      owner => "vagrant"
    }
    
    file { "/home/vagrant/.bundle":
      ensure => directory,
      owner => "vagrant"
    }
    
    exec {"bundle install":
      command   => "bundle --binstubs=/home/vagrant/bin --path=/home/vagrant/.bundle",
      cwd       => "/vagrant",
      user      => "vagrant",
      timeout   => 0,
      require   => [Rbenv::Build["2.1.1"], File["/home/vagrant/bin"], File["/home/vagrant/.bundle"]]
    }
    
    Package <| |> -> Exec["bundle install"]
    
  18. We’re almost done! Now would be a good time to destroy that dummy vagrant box we started in the beginning. Run vagrant destroy.

  19. Now run vagrant up to build the box from scratch, with all of our configurations in place. This might take a while, since it’s downloading and installing every thing for your app.

  20. Once the box has finished provisioning, you can ssh into it vagrant ssh.

  21. In the box, run cd /vagrant, since this is where your app lives on the virtual box. From there, run your migrations rake db:migrate, and then you should be good to go to run your specs and even start your rails server locally.

  22. Run rails s in the /vagrant directory of your virtual box. Now go to the browser on your local machine, and go to localhost:3000. Your app should be up and running!

  23. And finally, some housekeeping: make sure that the .vagrant/ and modules/ directories are listed in your .gitignore file.

It may not seem that way (23 steps later), but this is a pretty basic setup for a Rails application. Of course you can add more bells & whistles. For example, you may want install git on your virtual machine to be able to run git commands both locally and from your box, but I’ll save that for a future post...

Now when somebody new joins your project, all they have to do is clone your repository, install the librarian-puppet vagrant plugin, and run vagrant up. Sweet, right?

Please reach out to me if you’ve found any mistakes or issues—I’m happy to fix.