WordPress deployment with Ansible

A WordPress installation is fairly simple, and virtually all shared hosting providers support running WordPress out of the box. But when you use a VPS such as DigitalOcean or Linode instead of shared hosting, you need to administer the server yourself. This means installing web server/MySQL/PHP etc., performing regular backups, and keeping the system up-to-date.

This time round for leontius.net I played around with Ansible and Docker. The website uses WordPress over standard Linux + Nginx + MySQL + PHP-FPM stack.

Why Ansible?

Ansible is an open source Software Configuration Management (SCM): it basically is a framework to ease managing common server maintenance tasks (software only!) such as provisioning, deployment, and regular maintenance. It is relatively new as compared to similar major open source offerings such as Puppet, Chef, and Salt. Wikipedia has a great curation and comparison of open source SCMs, by the way.

With SCMs, you “freeze” the definitions of your server configuration in a configuration file, which works like a script that can be re-run and can be put in version control system. If you have ever stored notes on how to provision servers or even some bash scripts, SCMs will be a natural evolution of that. SCMs are obviously immensely helpful if you manage a fleet of nodes, but even with single server deployments, they allow you to confidently and easily spin up new production nodes whenever required – for example, when moving hosts or datacenters.

Compared with the other SCMs, Ansible really shines in the simplicity department. There is no additional setup is required in managed nodes besides an SSH server (which is usually already there). You just need to install Ansible in control machines – typically your workstation initially.

In addition, the configuration files (Ansible calls these playboooks, Chef recipes, Puppet just config files) are written in human-readable YAML and is procedural in nature. This means actions follow the steps as presented in the file. Other SCMs are more declarative in the sense that they may execute actions out of order if dependencies are not specified – this is done in the name of parallelism. Unfortunately this also means that config files can be a lot more complex, like how a multi-threaded program compares with a single-threaded one.

Development and testing

To test the Ansible playbooks I run a simple Docker container with OpenSSH server. Docker is a wonderful tool that basically produces very lightweight virtual machines that can spin up in sub-second interval and shares resources (disk space, memory, CPU, etc) with the host; it is sort of half-way between a chroot and full-blown hypervisors like Xen. With Docker, you can easily tear off and bring up fresh VMs very quickly to ensure that whatever script that you wrote works from clean slate.

After you install Docker, spin up a new container like so:

$ docker run -d -p 2222:22 -p 8888:80 leonth/ssh

(You may need sudo.) This forwards port 22 and 80 of the container (“guest”) to the host 2222 and 8888, respectively. leonth/ssh is not published in the Docker Registry, so you cannot do that in your local machine just yet, but you can readily use other similar publicly available containers like tutum/ubuntu, e.g.

$ docker run -d -p 2222:22 -p 8888:80 tutum/ubuntu

The Dockerfile for leonth/ssh is available on GitHub, but it is actually quite simple and there are lots of variations over the Registry.

The end result Ansible playbook looks something like this:

---
- hosts: all
  user: root
  pre_tasks:
  - name: install python-apt (bootstrap for apt tasks)
    command: apt-get install -y python-apt

  roles:
  - role: Ansibles.mysql
    mysql_databases:
    - name: leontius_net
    mysql_users:
    - name: leontius_net
      pass: "mysql DB password here"
      priv: "leontius_net.*:ALL"

  tasks:
  - name: install required packages
    apt: pkg={{item}} state=present
    with_items:
    - nginx
    - nginx-extras # for upload_progress
    - php5-fpm
    - php5-gd
    - php5-curl # for WP google analytics plugin
    - php5-mcrypt # for WP backup to dropbox
    - php-apc # optimization

  - include: enable-nginx-site.yml site=leontius.net

  - name: ensure wp-uploads directory exists and is writeable
    file: state=directory path=/var/www/sites/leontius.net/wp-content/uploads mode=744 owner=www-data

  - name: start daemons
    service: name={{item}} state=started
    with_items: [nginx, php5-fpm, mysql]

  handlers:
  - name: reload nginx
    service: name=nginx state=reloaded
---
- name: '{{site}}: copy nginx config'
  copy: src='{{site}}/nginx-config' dest='/etc/nginx/sites-available/{{site}}'
  notify: reload nginx

- name: '{{site}}: make sure nginx sites-enabled directory exists'
  file: state=directory path=/etc/nginx/sites-enabled

- name: '{{site}}: enable nginx site'
  file: state=link force=yes src=/etc/nginx/sites-available/{{site}} dest=/etc/nginx/sites-enabled/{{site}}
  notify: reload nginx

the playbook depends on Ansibles.mysql role, which performs standard hardening (MySQL fresh installs tend to be insecure) in addition to creating databases and users on the fly. It can be downloaded by executing:

$ ansible-galaxy install -p roles Ansibles.postgresql

roles is a folder where Ansible searches roles by default. More about roles.

The playbook is fairly self explanatory as each action (or “task”) has a comment on it. This style is encouraged within Ansible itself so you can expect playbooks in the wild to have similar readability.

After everything is set, you can run the playbook like so:

$ ansible-playbook -i local.inventory.cfg setup.playbook.yml

local.inventory.cfg contains information about the managed nodes, in this case our Docker container:

172.17.42.1:2222 mysql_leontius_net_password=password_here

Deployment

Simply create another inventory file e.g. prod.inventory.cfg, put production nodes particulars, and run as per above e.g.

$ ansible-playbook -i prod.inventory.cfg setup.playbook.yml

If you have, say, 100 nodes to manage, simply add their particulars in the inventory file and Ansible will manage those 100 nodes using the same playbook.

Unfortunately the current playbook does not upload the WordPress code base automatically – you need to do it yourself and install WordPress manually. Ansible certainly is capable of installing WordPress unattended; I consider that an overkill though as most likely I will only do it once (subsequently there may be migrations and upgrades, which may need a different playbook than the one of install).

Not deploying with Docker?

I have attempted to deploy using Docker containers (i.e. daemon processes run in containers in production) but I find it to be too much of a hassle for a simple configuration like mine. Using containers add one more layer of indirection when you want to do things like viewing logs as well as backing up files and databases, without significant advantage. It surely will be useful though if you need to run a few dozens of daemons and want to isolate them from each other – but not in the case of a blog.

Code

All code related to this post can be downloaded here.

Leave a Reply