Ansible WordPress Howto – Part 3


In previous posts in this series we discussed the basics for installing the main components of a highly available WordPress application, the application is deployed on a 3-tier infrastructure:


scalable wordpress architecture

scalable wordpress architecture


In the last post I installed the the main tools that will be used on the three tiers, so for load balancers I used haproxy, and for the application servers I installed nginx, php5, and hhvm. Finally for the database server I simply installed MySQL.


I this post I will finalize the setup by installing fresh instance of WordPress and by configuring the different components on the server to communicate with each other safely.


Playbook Recap

The Ansible playbook so far contains all the previously discussed details about the components on the servers. Let’s see the summary of the roles in this playbook, first clone the repository from github, and then run the following:


$ cd install-wordpress-ansible
$ tree . -L 2

├── droplets.yml
├── group_vars
│   ├── all.yml
│   ├── apps.yml
│   ├── dbs.yml
│   ├── lbs.yml
│   └── mysql_passwords.yml
├── hosts
├── playbook.yml
└── roles
├── common
├── fail2ban
├── haproxy
├── hhvm
├── iptables
├── mysql
├── nginx
├── openssh
├── php5
└── rkhunter


As you can see, all the roles are in the roles directory and the variables related to the each group defined in the hosts file are in group_vars directory. The playbook.yml file contains all the plays that will run on the servers:


# Common tasks
- hosts: all
  gather_facts: True
  sudo: yes
    - { role: common, tags: ["common"] }
    - { role: openssh, tags: ["openssh"] }
    - { role: fail2ban, tags: ["fail2ban"] }
    - { role: rkhunter, tags: ["rkhunter"] }
    - { role: iptables, tags: ["iptables"] }

# Load Balancer(s) roles
- hosts: lbs
  sudo: yes
    - { role: haproxy, tags: ["haproxy"] }

# Application Server(s) roles
- hosts: apps
  sudo: yes
    - { role: nginx, tags: ["nginx"] }
    - { role: php5, tags: ["php5"] }
    - { role: hhvm, tags: ["hhvm"] }

# Database Server(s) roles
- hosts: dbs
  sudo: yes
    - group_vars/mysql_passwords.yml
    - { role: mysql, tags: ["mysql"] }

Now it is time to connect the components together, and start running the application.


Application Role

In my work as a system administrator, I am using Ansible on a wide set of production systems. One of my “best practices” is to install every component with a separate role and to leave the communication between the components to one role. I use to call it “application role”.
The application role will contain different files in the tasks directory, each file will be responsible for certain tasks. For example I am using a file called nginx.yml to install Nginx virtual hosts on the application servers, and wordpress.yml to download the WordPress application and configure the application to connect to the database servers.


This role will run on the three servers, and will split the tasks according to the server roles, the server roles are defined in the inventory file like this:









The role consists of 6 different tasks other than the main.yml, these tasks are:


  • 01-hosts_file.yml
  • 02-create_databases.yml
  • 03-app_dir.yml
  • 04-install_wp.yml
  • 05-nginx_vhost.yml
  • 06-add_haproxy.yml


Now we will discuss each task in more detail …


common tasks

The common task that will run on all servers is 01-hosts_file.yml, which is responsible of constructing the /etc/hosts file and adding the ips of the servers in it:


- name: Add lbs servers ips to hosts file
  lineinfile: dest=/etc/hosts insertafter="ff02::2 ip6-allrouters" line="{{ hostvars[item]['ansible_eth1']['ipv4']['address'] }} {{item}}" backup=yes
  with_items: groups['lbs']

- name: Add apps servers ips to hosts file
  lineinfile: dest=/etc/hosts insertafter="ff02::2 ip6-allrouters" line="{{ hostvars[item]['ansible_eth1']['ipv4']['address'] }} {{item}}" backup=yes
  with_items: groups['apps']

- name: Add dbs servers ips to hosts file
  lineinfile: dest=/etc/hosts insertafter="ff02::2 ip6-allrouters" line="{{ hostvars[item]['ansible_eth1']['ipv4']['address'] }} {{item}}" backup=yes
  with_items: groups['dbs']


As you might notice, a special variable was added to denote the IP address of the eth1 interface or the private network interface. The hostvars is a “magic” variable that can list all the facts related to a host from the inventory file. For more information on “magic” variables please refer to the documentation.


Database server tasks

The task in this section is 01-create_database.yml which will create a database and get the password from an encrypted file that we created in the last post:


- name: create a database for wordpress
  mysql_db: name={{ db_name }} state=present
- name: create database user for wordpress
  mysql_user: name={{ db_user }} password={{ db_password }} state=present priv={{ db_name }}.*:ALL host='%'


Note: to add the db_password to the encrypted password file, you should use ansible-vault like the following:


$ ansible-vault edit --vault-password-file vault_pass.txt group_vars/passwords.yml


Note that I added the password of the vault to vault_pass.txt to make it more easy to decrypt the password file.


# Mysql passwords
mysql_root_password: wordpress-secret
db_password: sitepassword


Application server’s tasks

These tasks are responsible for adding the configuration for nginx, creating the document root, downloading a fresh installation of WordPress, and configuring WordPress to connect to the database.


The first task file is the 03-app_dir.yml which will create a document root directory for the application:


- name: create document root for the application
  file: path={{ app_dir }} state=directory owner={{ app_user }} group={{ app_user }} recurse=yes


The next task file 04-install_wp.yml is to download the latest version of wordpress and extract it inside the document root, and finally add the configuration file (wp-config.php) to add the database credentials:

- name: Download WordPress to application dir
  get_url: url= dest=/tmp/latest.tar.gz mode=0644
- name: Unarchive WordPress
  command: chdir={{app_dir}} creates={{app_dir}}/index.php tar --strip 1 -xvzf /tmp/latest.tar.gz
- name: Change app dir permissions
  file: recurse=yes path={{app_dir}} owner={{app_user}} group={{app_user}} state=directory
- name: configure wordpress
  template: src=wp-config.php.j2 dest={{app_dir}}/wp-config.php owner={{app_user}} group={{app_user}}
  notify: Restart hhvm


Configuring WordPress will require the wp-config.php to be added to the document root, so we added it as a template:


define('DB_NAME', '{{ db_name }}');
define('DB_USER', '{{ db_user }}');
define('DB_PASSWORD', '{{ db_password }}');
define('DB_HOST', 'db1');
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');

{{ lookup('file', 'wp_salts.txt') }}

$table_prefix  = 'wp_';
define('WP_DEBUG', false);

if ( !defined('ABSPATH') )

define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');


Note that wp_salts.txt is the file containing all the salts and keys, and I used the function lookup to read all the values from this file.

The final task file is 05-nginx_vhost.yml which will add the Nginx and hhvm configuration files to Nginx directory:


- name: add the vhost
  template: src=wp_example.conf.j2 dest=/etc/nginx/sites-available/wp_example.conf
    - Restart Nginx
- name: add hhvm configuration file
  template: src=hhvm.conf.j2 dest=/etc/nginx/hhvm.conf
    - Restart Nginx
- name: enable the vhost on app only
  file: src=/etc/nginx/sites-available/wp_example.conf dest=/etc/nginx/sites-enabled/wp_example.conf state=link
    - Restart Nginx


Load Balancer Task

The task file here 06-add_haproxy.yml will add the haproxy.cfg template:


- name: Add haproxy configuration
  template: src=haproxy.cfg.j2 dest=/etc/haproxy/haproxy.cfg owner=root group=root mode=644
  notify: Restart haproxy


The haproxy configuration template will be something like that:

log /dev/log    local0
log /dev/log    local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy

log    global
mode    http
option    httplog
option    dontlognull
timeout connect 5000
timeout client  50000
timeout server  50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http

frontend http-in
bind *:80
mode http
default_backend apps

backend apps
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]

{% for host in groups['apps'] %}
  server {{ host }} {{ hostvars[host]['ansible_eth1']['ipv4']['address'] }}
{% endfor %}

listen stats
bind *:8888
stats enable
stats uri /
stats hide-version
stats auth {{ haproxy_admin_user }}:{{ haproxy_admin_password }}

The file is pretty simple but the interesting part is using a for loop to iterate over the app servers and add their addresses to the backend pool. You can learn more about using for loops in jinja2 templating here.


Also note that we added the port 8888 as a stats port to the Web monitoring UI of haproxy, so we should give access to this port in the firewall rules. So in group_vars/lbs.yml edit the variables to be something like that:


- name: SSH
port: 22
- name: HTTP
port: 80
- name: HTTPS
port: 443
port: 8888


We also added the haproxy admin username and password to the variables.


Running the playbook


We can now run the Ansible playbook to our group of servers. Use the following command to run the playbook:


$ ansible-playbook -s --vault-password-file vault_pass.txt -i hosts playbook.yml


After running playbook, you will have a fully functional WordPress application, you can check your installation using the load balancer url in the browser.
And also you can check the port 8888 for your haproxy admin panel.


Final touches


One important task in the operations world, is to plan a regular backup for your files and database. This can be done by only uploading a backup script that will be scheduled daily to take backup from your files and databases.
This will be done by creating a backup role in Ansible that will ensure to do this task. The Ansible role will upload backup- and cleanup-scripts to rotate the backups and not to fill disk space.


The backup for files and databases will be defined by two variables:


  • backup_files
  • backup_dbs


Which has to be set to True in order to upload scripts and schedule the cleanup, the main task will be something like that:


# Files backup
- name: create files backup directory
  file: path={{ backup_files_dest }} recurse=yes state=directory
  when: backup_files == True
- name: upload files backup script
  template: dest=/usr/local/bin/ mode=0700
  when: backup_files == True
- name: upload file cleanup script
  template: dest=/usr/local/bin/ mode=0700
  when: backup_files == True
- name: schedule file backup
  cron: name="Backup files" minute="0" hour="3" day="*" job="/usr/bin/ionice -c2 -n7 /usr/local/bin/"
  when: backup_files == True
- name: schedule file cleanup
  cron: name="Cleanup files" minute="0" hour="4" day="*" job="/usr/bin/ionice -c2 -n7 /usr/local/bin/"
  when: backup_files == True

# Database backup
- name: create dbs backup directory
  file: path={{ backup_dbs_dest }} recurse=yes state=directory
  when: backup_dbs == True
- name: upload database backup script
  template: dest=/usr/local/bin/ mode=0700
  when: backup_dbs == True
- name: upload database cleanup script
  template: dest=/usr/local/bin/ mode=0700
  when: backup_dbs == True
- name: schedule database backup
  cron: name="Backup files" minute="0" hour="3" day="*" job="/usr/bin/ionice -c2 -n7 /usr/local/bin/"
  when: backup_dbs == True
- name: schedule database cleanup
  cron: name="Cleanup files" minute="0" hour="4" day="*" job="/usr/bin/ionice -c2 -n7 /usr/local/bin/"
  when: backup_dbs == True

Note that I used ionice to schedule the backup for files and databases with low cpu priority to avoid any downtime during the backup or cleanup operations.


MySQL Replication

We talked about adding a replication solution to our application – at the moment this is not part of this tutorial and will be added later.


You can find detailed documentation about setting up replication for MySQL in the official MySQL documentation that can help you get this process done.




Ansible is a simple yet really powerful tool that can be used to automatically build multi layer infrastructure solutions. I will combine all 3 articles into a video – stay tuned …