Mugo Web main content.

Building a development environment from a production website with Vagrant and VirtualBox

By: Philipp Kamps | November 16, 2015 | Business solutions and Case study

When it comes to local development environments, in a lot of cases Mugo uses VirtualBox images and manages them with Vagrant. These environments are sometimes created after the production environment has been set up. To make a local development environment as similar to production as possible, one approach is to actually copy the production server.

VirtualBox is a virtualization product that runs on all major operating systems. For example, on my Mac OS laptop I can create a virtual machine running Red Hat Enterprise Linux with all project-required services like PHP, MySQL, Solr, and so on.

Vagrant allows me to configure, download, start, and stop a VirtualBox image. It is also responsible for sharing the project source code between the virtual machine and the host operating system. For example, on my Mac OS laptop I use PhpStorm to work with the project source code. The source code is shared via NFS with the virtual machine, so that all of the code changes I make in PhpStorm directly affect the local development environment.

Copying from production to the local environment

There are many ways to create a local virtual machine setup. For example, you can use Ansible to configure the virtual machine as described in another blog post. Or, you can use only VirtualBox and build the virtual machine manually. Another possibility, as I will describe in this post, is to convert the raw contents of the production or staging hard drive into a VirtualBox image file. Basically, you build a copy of your production or testing environment containing all services, OS configurations, data, and source code.

Here is an example shell script I wrote for an eZ Publish project:

#!/bin/sh

echo 'Stop services'
/etc/init.d/mysqld stop
/etc/init.d/httpd stop
/etc/init.d/solr stop
/etc/init.d/crond stop

echo '"Defrag" disks'
cd /tmp; cat /dev/zero > zero.file; sync; rm zero.file; sync;
cd -
cd /media/data; cat /dev/zero > zero.file; sync; rm zero.file; sync;
cd -

echo 'Build data image'
rm tmp/data.img
dd if=/dev/xvdf of=tmp/data.img
echo 'Build root image'
rm tmp/root.img
dd if=/dev/xvda of=tmp/root.img

echo 'Start services'
/etc/init.d/mysqld start
/etc/init.d/httpd start
/etc/init.d/solr start
/etc/init.d/crond start

echo 'Fix bad sectors'
#just making sure loop2 is available
losetup -d /dev/loop2 &> /dev/null
losetup -o 1048576 /dev/loop2 tmp/root.img
e2fsck -p /dev/loop2
losetup -d /dev/loop2
losetup /dev/loop2 tmp/data.img
e2fsck -p /dev/loop2
losetup -d /dev/loop2

echo 'Convert data img'
rm build/box-disk2.vmdk
VBoxManage convertfromraw tmp/data.img build/box-disk2.vmdk --format VMDK --variant Stream
echo 'Convert root img'
rm build/box-disk1.vmdk
VBoxManage convertfromraw tmp/root.img build/box-disk1.vmdk --format VMDK --variant Stream

echo 'Set disk UUIDs'
VBoxManage internalcommands sethduuid build/box-disk1.vmdk '6a69323a-0a25-4540-93c9-b6834553a1c9'
VBoxManage internalcommands sethduuid build/box-disk2.vmdk '7eaad47b-7475-44b8-80c9-a28b49bf3e79'

echo 'Package to examplesite.box'
tar -czvf .examplesite.box_build -C build .
mv -f .examplesite.box_build examplesite.box

echo 'done'

The basic idea is to use the linux tool dd to capture the raw hard-drive content into a file. Then, the VBoxManage tool, which comes with VirtualBox, turns the raw image into a VirtualBox image.

As you can see in the script, this is quite involved, as it does the following:

  • Stop all services to reduce the activity on the hard drive; this avoids errors in the dd image. This takes the site down temporarily of course, so you usually have to do this from the staging or testing environment.
  • Fill the entire hard drive content from /dev/zero; this reduces the resulting image size
  • Check the image for errors and fix those, as dd sometimes leaves some lost inodes
  • Set a specific UUID for the hard drive, to ensure that it matches your VirtualBox configuration
  • Make sure you have an additional hard drive to create the virtual machine

Having an exact copy of the production or testing environment for local development is great, in order to reduce any surprises when it's time to deploy your work. However, there are some challenges. One challenge is if you have a scheduled script that should be run on the production environment only. That's why the configuration or system scripts sometimes need to know the environment they are in. Here is an example that checks for the presence of VirtualBox:

machineID='lspci | grep -o 'VirtualBox Graphics Adapter''

if [ "$machineID" == "VirtualBox Graphics Adapter" ]; then
    #I'm a virtual machine
fi

Editing site code

Usually, we use Vagrant to mount the source code from the host operating system to the virtual machine. You can also do the opposite: set up an NFS server on the virtual machine and tell Vagrant to mount the share to the host OS. This unusual setup has some pros and cons:

  • Pro: Performance is much better when the code base is located directly on the virtual machine.
  • Pro: It is more straightforward to have Windows mount an NFS location than to have a Windows NFS server.
  • Con: The source code is only available once the virtual machine is started. Sometimes you only want to implement simple code changes and it is a bit overkill to start the virtual machine for that. You should consider to check out another instance of the source code on your host OS for these types of code changes.
  • Con: Accessing the code base over NFS lowers performance for full text search or code synchronization with a remote code repository. This can be mitigated if you use an editor such as PhpStorm that creates its own search index.

Here is an example Vagrant configuration file to manage the virtual machine, including the mount from the virtual machine to the host OS:

# -*- mode: ruby -*-
# vi: set ft=ruby :
#

Vagrant.require_version ">= 1.3.0"

# Define the host OS
module OS
  def OS.windows?
    (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
  end

  def OS.mac?
    (/darwin/ =~ RUBY_PLATFORM) != nil
  end

  def OS.unix?
    !OS.windows?
  end

  def OS.linux?
    OS.unix? and not OS.mac?
  end
end

unless Vagrant.has_plugin?("vagrant-hostmanager")
  raise "!*! Plugin required !*!\n\n\tvagrant plugin install vagrant-hostmanager\n"
end

unless Vagrant.has_plugin?("vagrant-triggers")
  raise "!*! Plugin required !*!\n\n\tvagrant plugin install vagrant-triggers\n"
end

Vagrant.configure("2") do |config|
  config.hostmanager.enabled = true
  config.hostmanager.manage_host = true
  config.hostmanager.ignore_private_ip = false
  config.hostmanager.include_offline = true
  config.vbguest.auto_update = false
  config.vm.define 'examplesite' do |node|
    node.vm.box = 'boxname'
    node.vm.box_url = "http://url.to/the/vagrant/box/file"
    node.vm.hostname = 'dev.project.com'
    node.vm.network "private_network", ip: "172.28.128.3"
    node.hostmanager.aliases = %w(alias.project.com)
  end

  config.ssh.username = "ec2-user"
  config.ssh.private_key_path = "id_ExampleKey.pem"

  config.vm.provider :virtualbox do |vb|
    vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on", "--memory", 4096]
   #vb.gui = true
  end

  # only an example - does not support Windows
  config.trigger.after :up do
    run "sudo mount -o resvport dev.project.com:/var/www ./share"
  end

  config.trigger.before :halt do
    run "sudo umount -f ./share"
  end
end