Multi-VM Vagrant the DRY way

So Vagrant is great. If you’re not using it yet, you should check it out. It allows you to develop your applications in the same environment they’ll be running in.

I’ve been using Vagrant and Chef for a little over a year now, but I hadn’t used Ruby much before. I’ve written a lot of Chef cookbooks and Vagrantfiles since then. Writing a multi-VM Vagrantfile is much easier if you lean on Ruby to help you keep them shorter and more readable.

Here’s an example of a Vagrantfile with 3 Ubuntu LTS 12.04 machines:

There are 3 parts to the Vagrantfile.

1: Box definitions

Boxes can be configured with several options, most of them optional. The schema is defined at the top of the file.

=begin
Example box JSON schema
{
    :name => :name_of_vagrant_box, #REQUIRED
    :ip => '10.0.0.11', #REQUIRED
    :synced_folders => [
        { '.' => '/home/vagrant/myapp' }
    ],
    :commands => [
        'touch /tmp/myfile'
    ],
    :vbox_config => [
        { '--memory' => '1536' }
    ],
    :chef_role => 'myrole',
    :chef_json => {
        'local' => true,
        'env_name' => 'development',
    }
}  
=end

boxes = [
    # Clean box, Ubuntu 12.04 LTS - for experimentation
    { 
        :name => :clean, 
        :ip => '33.33.33.80'
    },
    # MongoDB box, with Chef 'mongo' role
    { 
        :name => :mongo,
        :ip => '33.33.33.81',
        :chef_role => 'mongo'
    },
    # Jenkins master box, with Chef 'jenkins' role
    { 
        :name => :jenkins,
        :ip => '33.33.33.82',
        :vbox_config => [
            { '--memory' => '1536' }
        ],
        :chef_role => 'jenkins',
        :chef_json => {
            'jenkins' => {
                'server' => {
                    'user' => 'jenkins'
                }
            }
        }
    }
]

2: Global Chef JSON

All boxes will have this Chef JSON applied. The :chef_json parameter in the box definition will be merged with this, overwriting values if they conflict.

# Chef JSON config for all boxes
chef_json = {
    "local" => true,
    "env_name" => "development",
    "s3cmd" => {
        "user" => "vagrant"
    },
    "chef_client" => {
        "cron" => {
            "hour" => "4",
            "minute" => "0"
        }
    }
}

3: Vagrant configuration loop

Here is the main Vagrant configuration, that loops over the boxes defined in part 1 to create the Vagrant box definitions. If you’re provisioning your box via Chef it applies the chef_json hash and your box’s specific chef_json hash to apply attributes.

Vagrant.configure("2") do |config|
    boxes.each do |opts|
        config.vm.define opts[:name] do |config|
            # Box basics
            config.vm.box = "precise64"
            config.vm.box_url = "http://files.vagrantup.com/precise64.box"
            config.vm.network :private_network, ip: opts[:ip]
            config.ssh.forward_agent = true

            # Synced folders
            opts[:synced_folders].each do |hash|
                hash.each do |folder1, folder2|
                    config.vm.synced_folder folder1, folder2
                end
            end

            # VirtualBox customizations
            unless opts[:vbox_config].nil?
                config.vm.provider :virtualbox do |vb|
                    opts[:vbox_config].each do |hash|
                        hash.each do |key, value|
                            vb.customize ['modifyvm', :id, key, value]
                        end
                    end
                end
            end

            # Use a more recent version of Chef
            config.vm.provision :shell, :inline => "gem install chef --version 11.6.0 --no-rdoc --no-ri --conservative"

            # Run shell commands for box
            opts[:commands].each do |command|
                config.vm.provision :shell, :inline => command    
            end

            # Configure the box with Chef
            config.vm.provision :chef_solo do |chef|
                # Set paths to local Chef resources
                chef.provisioning_path = "/etc/chef"
                chef.cookbooks_path = ["../chef/cookbooks", "../chef/vendor/cookbooks"]
                chef.roles_path = "../chef/roles"

                # Add a Chef role if specified
                unless opts[:chef_role].nil?
                    chef.add_role(opts[:chef_role])   
                end

                # Merge the general Chef JSON with box-specific JSON
                chef.json = chef_json.merge(opts[:chef_json])
            end    
        end
    end
end

Benefits to writing your Multi-VM Vagrantfiles this way:

Of course you can do anything in your Vagrantfile that you can in any Ruby file, so I think you can extend this pattern to fit a lot of use cases. I am interested in suggestions on how to make this a better abstraction and hearing how you are tackling this problem.

The full Vagrantfile is up on Github.

 
39
Kudos
 
39
Kudos

Now read this

Lessons learned running a DevOps meetup

I’ve been organizing the Boston DevOps meetup for almost two years now. When the organizer spot opened up I jumped at the chance. My experience DevOps-ing along in smaller organizations gave me a pretty narrow view of the movement and I... Continue →