Using Vagrant for Consistent Development Environments
Vagrant is a command-line tool that can automate the creation of virtual machines for consistent and reproducible development environments.
If you’re working on a team of developers, this is invaluable and serves to eliminate the “It works on my machine!” issues that can disrupt work.
In this article, we’ll walk through setting up a basic development environment on our host (Windows) so that we can launch a guest VM (CentOS Linux) running Apache.
Before we get started, we need to become familiar with a few key concepts.
It’s important to know that Vagrant doesn’t provide any virtualization functionality. Instead, it acts as a wrapper to integrate with third-party virtualization products. These products are called providers. Some common ones are VirtualBox, Hyper-V, Docker, and VMware.
Boxes are the package format for Vagrant environments. We can think of them as a base image for a VM. There are boxes for every flavor of Linux and Windows.
This is the configuration file Vagrant uses to determine how the environment should be provisioned.
Download the latest version of Vagrant for your operating system.
The most common provider is VirtualBox so let’s use that for our example.
Download the latest version of VirtualBox for your operating system.
Choosing a Box
We’ll want to choose a box that closely resembles our production environment. There’s a public catalog of boxes to search from. Each publisher provides the name of their box.
Let’s use the offical CentOS 7 box to initialize our environment:
D:\vagrant> vagrant init centos/7
This vagrant init command performs the following actions:
- Creates a Vagrantfile in our current directory which is D:\vagrant
- Sets config.vm.box = centos/7 in the file
The default Vagrantfile comes with some standard settings that don’t need modifying in most cases. There are a few things to point out.
Vagrant allows us to share a folder from our host operating system to the guest VM so we can develop on it easily.
By default, Vagrant will mount the current working directory as /vagrant in the guest.
If you’re not familiar with VirtualBox, there are two modes of networking that can be configured for a VM — NAT and Bridged.
The default mode is NAT which means that the guest O/S will have an IP on its own separate network. We can setup port forwarding so that we can access the machine from our host via 127.0.0.1.
Go to the VirtualBox settings for the VM that was created. Under the Network tab, click the Port Forwarding button the Advanced menu for Adapter 1.
Notice that port forwarding is already configured for SSH on port 2222. This means that we can access port 22 on the guest VM by connecting to localhost:2222 from the host.
Since we’ll be running a web server, we need to add an entries for port 80 (http) and 443 (https). We could manually add these entries in the Network tab but that won’t make the settings persistent between installs.
So let’s add them to the Vagrantfile:
config.vm.network "forwarded_port", guest: 80, host: 80
config.vm.network "forwarded_port", guest: 443, host: 443
Running a bootstrap script
We can bootstrap our VM with a shell script that installs the web server and performs required configuration steps.
config.vm.provision "shell", path: "https://raw.githubusercontent.com/codebyamir/vagrant/master/bootstrap.sh"
This will download a sample bootstrap script from my GitHub repository to install Apache, mod_ssl, and re-point the DocumentRoot from /var/www/html to /vagrant.
Starting the VM
Now we’re ready to start the VM by running vagrant up:
D:\vagrant> vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'centos/7'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'centos/7' is up to date...
==> default: Setting the name of the VM: vagrant_default_1487471652452_83108
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
default: Adapter 1: nat
==> default: You are trying to forward to privileged ports (ports <= 1024). Most
==> default: operating systems restrict this to only privileged process (typically
==> default: processes running as an administrative user). This is a warning in case
==> default: the port forwarding doesn't work. If any problems occur, please try a
==> default: port higher than 1024.
==> default: Forwarding ports...
default: 80 (guest) => 80 (host) (adapter 1)
default: 443 (guest) => 443 (host) (adapter 1)
default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
default: SSH address: 127.0.0.1:2222
default: SSH username: vagrant
default: SSH auth method: private key
default: Warning: Remote connection disconnect. Retrying...
default: Vagrant insecure key detected. Vagrant will automatically replace
default: this with a newly generated keypair for better security.
default: Inserting generated public key within guest...
default: Removing insecure key from the guest if it's present...
default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Mounting shared folders...
default: /vagrant => D;/vagrant
==> default: Running provisioner: shell...
default: Running: C:/Users/amirb/AppData/Local/Temp/vagrant-shell20170218-10884-13tcyur.sh
==> default: Delta RPMs disabled because /usr/bin/applydeltarpm not installed.
==> default: Public key for apr-1.4.8-3.el7.x86_64.rpm is not installed
==> default: warning: /var/cache/yum/x86_64/7/base/packages/apr-1.4.8-3.el7.x86_64.rpm: Header V3 RSA/SHA256 Signature, key ID f4a80eb5: NOKEY
==> default: Public key for expat-2.1.0-10.el7_3.x86_64.rpm is not installed
==> default: Importing GPG key 0xF4A80EB5:
==> default: Userid : "CentOS-7 Key (CentOS 7 Official Signing Key) "
==> default: Fingerprint: 6341 ab27 53d7 8a78 a7c2 7bb1 24c6 a8a7 f4a8 0eb5
==> default: Package : centos-release-7-2.1511.el7.centos.2.10.x86_64 (@anaconda)
==> default: From : /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
==> default: Warning: RPMDB altered outside of yum.
==> default: Created symlink from /etc/systemd/system/multi-user.target.wants/httpd.service to /usr/lib/systemd/system/httpd.service.
This reads the Vagrantfile to determine what it should do. The first time you run this, it will take a few minutes because it needs to download the box image. Then it will create the VM, configure networking, and run our bootstrap script.
The downloaded box is stored in C:/Users/<username>/.vagrant.d/boxes in Windows.
Accessing the VM
There are a couple ways we can access the command-line of our Linux VM:
- VirtualBox console
- SSH using Putty to localhost on port 2222
- SSH using Windows command-line by running vagrant ssh
Let’s confirm we can hit the web server as well. Open a browser on the host and navigate to http://127.0.0.1. You should see the Apache welcome page.
Stopping the VM
To stop the VM, run the following vagrant halt
D:\vagrant> vagrant halt
==> default: Attempting graceful shutdown of VM...
This command shuts down the running VM that Vagrant is managing in a graceful manner through the guest O/S shutdown hook.
Deleting the VM
To delete the VM, run the following:
D:\vagrant> vagrant destroy
default: Are you sure you want to destroy the 'default' VM? [y/N] y
==> default: Destroying VM and associated drives...
This will delete the VM from VirtualBox so all changes that were made to the guest will be lost.
Here’s an example of a basic workflow for a web developer:
- Launch development environment using Vagrant/VirtualBox
- Update code on host operating system
- Test code using guest VM
- Commit code to Git repository
There’s a known bug in VirtualBox which can result in files not syncing properly in shared folders. You’ll see this behavior if you’ve updated content in a shared folder but don’t see the update in the VM (or vice versa).
The workaround is to deactivate sendfile in Apache:
# echo "EnableSendfile off" > /etc/httpd/conf.d/disablesendfile.conf
# systemctl reload httpd.service