A common method of testing websites when developing, is to use a local PHP server such as XAMPP, or the built in server provided with PHP. However, there are times when just using $ php -S localhost:4000
doesn't quite cut it. Especially when external services are required such as Memcached, Redis or a database. It's easy to install these services locally on your Mac using tools like Homebrew, but after a while, its only going to clog up your machine.
There are also scenarios where you may want to test updated software packages without affecting a production environment, for example, when a newer version of PHP is released, or when investigating switching from Node.js to io.js.
Vagrant to the rescue!
Vagrant is a tool for building complete development environments. With an easy-to-use workflow and focus on automation.
https://www.vagrantup.com/about.html
Vagrant can be used to create and configure lightweight, reproducible, and portable development environments. It makes it possible to closely replicate your production environment, using a local virtual machine
The following packages are required:
$ brew install ansible
)$ brew install caskroom/cask/brew-cask && brew cask install vagrant
)I've created a repo on Github containing all the examples in this post, plus some additional tasks. Feel free to clone the repo, run $ vagrant up --provision
and see for yourself.
Once Vagrant has been installed, getting started is as easy as navigating to your desired folder and running:
$ vagrant init ubuntu/trusty64
$ vagrant up
This will create a virtual machine running Ubuntu 14.04, and symlink the current directory to the /vagrant/
folder on the VM.
Note: Using a box for the first time might take a while, as it has to be downloaded. Don't let this put you off. Once the box has been downloaded, it can be used for multiple Vagrant machines without needing to be downloaded again and will load in seconds.
The Vagrantfile
created by the above commands will be minimal, however, it will be full of comments and examples of what can be done.
Here is an example of a trimmed down Vagrantfile
containing only the bare essentials. It has been configured to use Ubuntu 14.04. The config.vm.network
option is set to use a public network, meaning that the machine will be accessible via other computers within your house, or on the network. config.vm.network
has also been set to forward traffic from the port 8080 on your physical machine, to port 80 on the virtual machine.
# -*- 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/trusty64"
config.vm.network :public_network
config.vm.network "forwarded_port", host: 8080, guest: 80
end
Vagrant only does part of the work, creating the machines, and obviously by default, a box will not be configured perfectly for everyone's individual use cases. This is where provisioning comes in, allowing you to do things such as automatically install software and alter configurations.
Vagrant supports a wide range of options when provisioning machines, including Puppet, Chef, Salt, Ansible and the shell. We will be using Ansible in this example as it is very easy to get started with, but very powerful.
The main reason for this is because Ansible is agentless. This means that no additional packages need to be installed on the machines that will be provisioned, all the work is done via SSH. Even though it is agentless, it still provides idempotency, meaning that it can be run multiple times without affecting the state.
Lets create a playbook that will be used to define which hosts should be configured, the roles, and the tasks belonging to each role. Create a folder called ansible
, and a file inside that called playbook.yml
. Also, we need to tell Vagrant that we will be using Ansible as our provisioner. Add the following lines to the Vagrantfile
:
config.vm.provision "ansible" do |ansible|
ansible.playbook = "ansible/playbook.yml"
end
This sets Ansible as the provider, and points Vagrant to our playbook.
Next, we need to add some contents to the playbook.yml
file, like so:
---
- hosts: all
This means that all available hosts should be provisioned, however, by default Vagrant limits this to the VM that was created.
Next, we can add our first task. Before we start installing software, it's always a good idea to ensure that the apt package manager is up to date. This is as simple as adding the following task:
---
- hosts: all
tasks:
- name: Update apt
apt: update_cache=yes
sudo: true
The apt
package manager features its own module built right into the core of ansible, more information about this module can be found on the documentation page.
For the next step, let's make sure that the latest version of PHP and some essential packages are installed.
- name: Ensure php5 is up to date
apt: pkg={% raw %}{{ item }}{% endraw %} state=latest
with_items:
- php5-cli
- php5-mcrypt
- php5-fpm
- php5-mysql
sudo: true
Rather than having to write four separate tasks for each PHP package, we can use the {% raw %}{{ item }}{% endraw %}
placeholder, and provide a list of packages. When running Ansible, if it finds and installs a newer version of PHP, we need to make sure that any services are restarted, so that the processes use the updated version. This can be done with a handler
, which is only executed when the state of a task changes.
notify: restart php
and add the handler:
---
- hosts: all
...tasks...
handlers:
- name: restart php
service: name=php5-fpm state=restarted
sudo: true
---
As more tasks are added, it is harder to follow what is happening in the playbook. Luckily, Ansible has support for defining roles. Roles allow you to split up the tasks and handlers into separate files and folders. It also possible to assign different roles to different servers within the inventory. For example, all servers could share a core
role which updates the apt cache. A select few of these could have a php
role that would ensure a LEMP or LAMP stack is present, up to date and correctly configured.
We can add a roles
folder, and start to create directories for each one, like so:
ansible
├── playbook.yml
└── roles
├── core
│ └── tasks
│ └── main.yml
└── php
├── handlers
│ └── main.yml
└── tasks
└── main.yml
Our updated playbook.yml
will then define the roles
---
- hosts: default
roles:
- core
- php
and the main.yml
file for each role will contain the tasks and handlers:
---
- name: Install php5
apt: pkg={% raw %}{{ item }}{% endraw %} state=latest
with_items:
- php5-cli
- php5-mcrypt
- php5-fpm
- php5-mysql
sudo: true
notify: restart php
---
- name: restart php
service: name=php5-fpm state=restarted
Finally, to see all of this in action, run $ vagrant up --provision
(or $ vagrant provision
if already running) and watch Ansible do its thing.
If you have any comments or questions, please get in touch.