Building a development environment from a production website with Vagrant and VirtualBox
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