LEMP with Vagrant and Ansible
Using Vagrant and Ansible to set up a LEMP server
LEMP is a variant of the common LAMP (Linux, Apache, MariaDB and PHP) bundle that swaps the Apache server with Nginx.
Many times I’ve used it to test some web application. Usually, you’d want to do this in a clean environment that won’t interfere with any previous configuration.
For this, you’d normally use some kind of virtual machine that you’ve installed and configured from scratch. Maybe, if it’s a common environment, you’d create a snapshot so you can revert to it afterwards. Or maybe you could use one of the many cloud images found in the Internet.
However, a much simpler option is to use Vagrant to handle these cloud images and a configuration management tool to handle their configuration.
Vagrant is an open-source software product for building and maintaining portable virtual development environments.
Vagrant can use different engines to boot up these cloud images, and also different tools for software provisioning. Here we will use VirtualBox and Ansible for these roles respectively.
Ansible is an open-source automation engine that automates software provisioning, configuration management, and application deployment.
On our host machine, we will only need to install Vagrant and VirtualBox, since Ansible will run in the guest machine. Therefore, we need to download and install the appropriate software for our operating system:
Configuration of Vagrant
Vagrant’s configuration is stored in a single file named Vagrantfile
.
First, we tell Vagrant to use VirtualBox as the default provider:
ENV['VAGRANT_DEFAULT_PROVIDER'] = 'virtualbox'
Then, we start the actual configuration by selecting the base cloud image we will be using. For this example, we use the official Ubuntu Xenial 32-bit image:
config.vm.box = 'ubuntu/xenial32'
To configure the virtual machine hardware (512 MB of RAM and a single CPU capped to 50%), we add the following:
config.vm.provider :virtualbox do |vbox|
vbox.memory = 512
vbox.cpus = 1
vbox.customize ['modifyvm', :id, '--cpuexecutioncap', '50']
end
Now we configure the hostname and IP address of the guest OS:
config.vm.define 'lemp' do |node|
node.vm.hostname = 'lemp'
node.vm.network :private_network, ip: '172.28.128.10'
node.vm.post_up_message = 'Web: http://172.28.128.10'
end
We will also share the local subdirectory vagrant
with the guest so it’s
mounted at /vagrant
:
config.vm.synced_folder 'vagrant', '/vagrant'
Finally, we configure Ansible to be run locally on the guest using the
configuration found in /vagrant/cfg
. In this directory, it will find the
inventory file hosts.ini
and the playbook file site.yml
. We will also tell
it to run all tasks using sudo:
config.vm.provision :ansible_local do |ansible|
ansible.provisioning_path = '/vagrant/cfg'
ansible.inventory_path = 'hosts.ini'
ansible.playbook = 'site.yml'
ansible.sudo = true
end
In the end, the file should look like this:
ENV['VAGRANT_DEFAULT_PROVIDER'] = 'virtualbox'
Vagrant.configure('2') do |config|
config.vm.box = 'ubuntu/xenial32'
config.vm.provider :virtualbox do |vbox|
vbox.memory = 512
vbox.cpus = 1
vbox.customize ['modifyvm', :id, '--cpuexecutioncap', '50']
end
config.vm.define 'lemp' do |node|
node.vm.hostname = 'lemp'
node.vm.network :private_network, ip: '172.28.128.10'
node.vm.post_up_message = 'Web: http://172.28.128.10'
end
config.vm.synced_folder 'vagrant', '/vagrant'
config.vm.provision :ansible_local do |ansible|
ansible.provisioning_path = '/vagrant/cfg'
ansible.inventory_path = 'hosts.ini'
ansible.playbook = 'site.yml'
ansible.sudo = true
end
end
Configuration of Ansible
Since we are sharing the subdirectory vagrant
with the guest machine, we need
to place all configuration files for Ansible inside vagrant/cfg
as specified
in Vagrantfile
.
Ansible’s inventory file contains the machines in which it will run. In this case, it will only run locally on one machine so we add it:
lemp ansible_connection=local
Also, Ansible’s playbooks store the steps to be taken on the machines. We could put everything in this file, but Ansible’s Best Practices recommend using roles:
---
- name: Configure LEMP server
hosts: lemp
roles:
- mariadb
- php
- nginx
Here we specify that this task will apply to the machine named lemp and that it will execute the roles mariadb, php and nginx.
MariaDB role
This role will install and configure MariaDB. Its configuration lives in the
subdirectory vagrant/cfg/roles/mariadb
:
vagrant/cfg/roles/mariadb
├── handlers
│ └── main.yml
├── tasks
│ └── main.yml
└── vars
└── main.yml
The tasks to be run are saved in tasks/main.yml
:
---
- name: Install server
package: name={{ item }} state=present
with_items:
- mariadb-server
- python-mysqldb
notify:
- start mysql
- name: Change root password
mysql_user:
name: root
host: localhost
password: '{{ mysql_root_password }}'
state: present
- name: Change bind-address
replace:
dest: /etc/mysql/mariadb.conf.d/50-server.cnf
regexp: '^bind-address'
replace: 'bind-address = {{ mysql_bind_address }}'
notify:
- restart mysql
- name: Create test database
mysql_db: name={{ mysql_db_name }} state=present
- name: Create test user
mysql_user:
name: '{{ mysql_db_user }}'
host: '%'
password: '{{ mysql_db_password }}'
priv: '{{ mysql_db_name }}.*:ALL'
state: present
These are the steps taken:
- First, using the package module, we install the necessary software.
- Then, using the mysql_user module, we change MariaDB’s root password to the one in the variable mysql_root_password.
- To be able to access the server from our host machine, we use the replace module to modify MariaDB’s configuration file and change its bind-address to the one in the variable mysql_bind_address.
- We then create a test database, using the mysql_db module.
- And finally we create a test user, again, using the mysql_user module.
All the variables we use in this role can be set in vars/main.yml
:
---
mysql_root_password: 'root'
mysql_bind_address: '0.0.0.0'
mysql_db_name: 'test'
mysql_db_user: 'test'
mysql_db_password: 'test'
Also, we define some handlers which are basic tasks that are run when another task changes something and notifies the handler. We use them to make sure the server is enabled and to restart it when we change the configuration:
---
- name: start mysql
service: name=mysql enabled=yes state=started
- name: restart mysql
service: name=mysql state=restarted
PHP role
This role will install PHP. Its configuration lives in the subdirectory
vagrant/cfg/roles/php
:
vagrant/cfg/roles/php
├── handlers
│ └── main.yml
└── tasks
└── main.yml
Using the package module, it installs PHP-FPM (FastCGI Process Manager) and the module to communicate with MySQL:
---
- name: Install PHP
package: name={{ item }} state=present
with_items:
- php-fpm
- php-mysql
notify:
- start php-fpm
We also define the handler that will make sure the service is enabled:
---
- name: start php-fpm
service: name=php7.0-fpm enabled=yes state=started
Nginx role
This role will install and configure Nginx. Its configuration lives in the
subdirectory vagrant/cfg/roles/nginx
:
vagrant/cfg/roles/nginx
├── handlers
│ └── main.yml
├── tasks
│ └── main.yml
├── templates
│ └── default
└── vars
└── main.yml
The tasks in tasks/main.yml
are:
---
- name: Install server
package: name={{ item }} state=present
with_items:
- nginx
notify:
- start nginx
- name: Change default configuration
template:
src: default
dest: /etc/nginx/sites-available/default
notify:
- reload nginx
Once again, using the package module, it will install the necessary
software. Then, using the template module, it changes the default’s site
configuration by copying our template from templates/default
:
# Default server configuration
server {
listen {{ http_port }} default_server;
listen [::]:{{ http_port }} default_server;
root {{ base_dir }};
index index.html index.htm index.php;
server_name _;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.0-fpm.sock;
}
location ~ /\.ht {
deny all;
}
}
The variables used in the template can be set in vars/main.yml
:
---
http_port: 80
base_dir: '/vagrant/www'
This way, any file we save in the subdirectory vagrant/www
of our host
machine will be accessible in the guest machine’s web server. We can work with
our favorite development tools locally and see all changes immediately
in the web server.
Finally, we define the handlers that will make sure the server is enabled and the configuration reloaded when we make any change:
---
- name: start nginx
service: name=nginx enabled=yes state=started
- name: reload nginx
service: name=nginx state=reloaded
Using Vagrant
Once everything is set up, we just have to start the guest machine with the
command vagrant up lemp
.
The first time we run it, it will download the necessary cloud image, so it might take a while. Subsequent boots will only check whether we have an updated version of the image.
Once it finishes booting up, we can connect to the web server in
http://172.28.128.10. For testing purposes, let’s say we’ve saved this in
vagrant/www/index.php
:
<?php phpinfo(); ?>
When we connect to the server with our web browser, we will see something like this:
We can also connect to the database server by running:
mysql --host=172.28.128.10 --user=test --password test
And, after providing our password, we will be able to enter SQL commands:
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 32
Server version: 10.0.29-MariaDB-0ubuntu0.16.04.1 Ubuntu 16.04
Copyright (c) 2000, 2016, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [test]>
To control the guest machine, here are the most important Vagrant commands:
Action | Command |
---|---|
boot guest machine | vagrant up lemp |
reboot guest machine | vagrant reload lemp |
shutdown guest machine | vagrant halt lemp |
boot and reconfigure guest machine | vagrant up lemp --provision |
connect to guest machine with SSH | vagrant ssh lemp |
destroy guest machine | vagrant destroy lemp |
Conclusion
Coupling Vagrant with Ansible (or any other SCM tool) allows for a portable reproducible system, contained in just a few text files:
.
├── [ 66K] vagrant
│ ├── [ 58K] cfg
│ │ ├── [ 37] hosts.ini
│ │ ├── [ 54K] roles
│ │ │ ├── [ 17K] mariadb
│ │ │ │ ├── [4.1K] handlers
│ │ │ │ │ └── [ 133] main.yml
│ │ │ │ ├── [4.7K] tasks
│ │ │ │ │ └── [ 759] main.yml
│ │ │ │ └── [4.2K] vars
│ │ │ │ └── [ 162] main.yml
│ │ │ ├── [ 21K] nginx
│ │ │ │ ├── [4.1K] handlers
│ │ │ │ │ └── [ 131] main.yml
│ │ │ │ ├── [4.3K] tasks
│ │ │ │ │ └── [ 263] main.yml
│ │ │ │ ├── [4.4K] templates
│ │ │ │ │ └── [ 397] default
│ │ │ │ └── [4.1K] vars
│ │ │ │ └── [ 70] main.yml
│ │ │ └── [ 12K] php
│ │ │ ├── [4.1K] handlers
│ │ │ │ └── [ 79] main.yml
│ │ │ └── [4.1K] tasks
│ │ │ └── [ 139] main.yml
│ │ └── [ 93] site.yml
│ └── [4.1K] www
│ └── [ 144] index.php
└── [ 748] Vagrantfile
No more messing with installers, restoring snapshots or reconfiguring stuff. You can boot up a fresh system, mess it up, destroy it and boot it up brand new again in a few minutes. You can even use a version control system to store these files and share them with others.
Also, official images for many operating systems can be found in Vagrant’s
website. Not just for Ubuntu but also for
Debian, Fedora, CentOS and
FreeBSD. You can even specify your own box with the setting
config.vm.box_url
.
At the same time, Ansible’s myriad of modules let us configure the guest OS automatically in almost any way possible, even though we may need to adapt many of the tasks to specific Linux distros or operating systems.
In the end, this method greatly simplifies the process of creating and managing test environments.
Further reading
- Creating a Base Box - VirtualBox Provider - Vagrant by HashiCorp
- Box Format - VMware Provider - Vagrant by HashiCorp
- Creating a Base Box - Hyper-V Provider - Vagrant by HashiCorp
- GitHub - vagrant-libvirt/vagrant-libvirt: Vagrant provider for libvirt
- GitHub - fgrehm/vagrant-lxc: LXC provider for Vagrant