Running Clustered WordPress on Windows Azure Virtual Machines

We’ve been running our WordPress corporate site on GoDaddy’s shared hosting and had been experiencing really slow response times (around 12 seconds with empty cache) so as a weekend project we decided to move over from GoDaddy to Windows Azure. Windows Azure is a great platform but unfortunately, is a little biased towards Microsoft products (understandably) when it comes to using their cloud services or websites. Azure does offer WordPress as part of Azure website but it requires a ClearDB MySql add-on for the back-end. ClearDB is fairly expensive, with their Jupiter Plan (10GB with 50 connections) being 99.99$ per month. Given the pretty steep pricing of ClearDB MySql (required for WP) we decided to run WordPress on Azure Virtual Machines with database hosted on VM itself. Since we are hosting a corporate website and any downtime is bad for business, we decided to host WP on multiple VMs. Sections below list out the steps that are needed for getting WordPress up & running on more than one Azure VM instance:

Technology stack

WP has a fairly standard tech stack (LAMP) but we replaced Apache with nginx and MySql with MariaDB. nginx is much faster in serving static content (and we are more comfortable with), integrates nicely with PHP via CGI (PHP-FPM) and mariadb is a drop-in replacement for MySql and setting up Clustering is a breeze using mariadb and galera.

Deployment architecture

wordpress

Let’s get started!

Provisioning VMs

Since we’d be running more than one VM instance for our WordPress cluster, it is a good idea to create a Virtual Network if you don’t have one already. You can create a Virtual network using either the Azure management portal or using Azure Powershell.
It’s always a good idea to provision VMs using a custom VHD which has all the software already installed so that we don’t have to repeat the steps on each and every VM, to do that, provision a new A0 VM, select Ubuntu 14.04 and select the Virtual Network that you’ve already created above. Once the VM is provisioned, ssh into the VM and install and cofigure the pre-requisites:

nginx

$ sudo apt-get install nginx

PHP Fast Process Manager along with any extensions that you need

$ sudo apt-get install php5-cgi php5-mysql php5-curl php5-gd php-pear php5-imagick php5-mcrypt php5-mhash php5-xmlrpc php5-fpm

MariaDB Galera Server

Trusty (14.04) does not include MariaDB Galera Server, so we will need to import the repo:

$ sudo apt-get install software-properties-common
$ sudo apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db
$ sudo add-apt-repository 'deb http://nyc2.mirrors.digitalocean.com/mariadb/repo/5.5/ubuntu trusty main'

Now we just need to install using apt-get:

$ sudo apt-get install mariadb-galera-server galera

Once the LEMP stack is installed, we need to de-provision this instance and capture the VHD so that we can use the same image for provisioning other instances:
$ sudo waagent -deprovision
Log on to Azure Manager, shutdown this VM and capture the disk, which will store the VHD in your Azure storage. Now you can provision two (or more) VM instances using the VHD that was captured, just make sure that while creating the instances you select your Virtual Network and use the custom VHD.

Setting up static IP

Given that we’d be setting a MariaDB cluster on these nodes, we will assign static IPs to our VMs. Static IPs can only be assigned using Azure Powershell as there is no way to do it via the management portal:

PS C:\> $staticVM = Get-AzureVM -ServiceName YourService -Name YourVM
PS C:\> Set-AzureStaticVNetIP -VM $staticVM -IPAddress 10.0.0.10 | Update-AzureVM

You can find more details on how to set static IP here.

Setting my MariaDB cluster

Now that both VMs are up and running MariaDB (albeit isolated), it’s time to setup the MariaDB cluster. Keep in mind, it is recommended to run MariaDB cluster on more than 2 nodes to avoid split-brain situations.
As the first step, we’ll first move the MariaDB’s data files to Azure Storage for redundancy. Firstly, we need to attach an empty disk to the VM (say 10GB) and initialize it. The steps for initializing a disk in Ubuntu are:
1. Create a new partition using fdisk
2. Create file system using mkfs (ext4 or ext3)
3. Mount the partition by editing /etc/fstab
You can read all these steps in detail over at the MSDN article.
Once the empty disk is attached to both the VMs, we need to the data directory of MariaDB (assuming that the new disk was mounted on /datadrive) by editing my.cnf file:
$ sudo nano /etc/mysql/my.cnf
Change the datadir entry to point to the new location (/datadrive/mysql). Copy all the files in current datadir directory to new location and restart MariaDB:

$ sudo cp -R -p /var/lib/mysql /datadrive
$ sudo service mysql restart

Setting up cluster

Stop mysql service on all VMs.
Create a new file at /etc/mysql/conf.d/cluster.cnf:

[mysqld]
query_cache_size=0
binlog_format=ROW
default-storage-engine=innodb
innodb_autoinc_lock_mode=2
query_cache_type=0
bind-address=0.0.0.0
# Galera Provider Configuration
wsrep_provider=/usr/lib/galera/libgalera_smm.so
# Galera Cluster Configuration
wsrep_cluster_name="test_cluster"
wsrep_cluster_address="gcomm://10.0.0.10,10.0.0.11"
# Galera Synchronization Congifuration
wsrep_sst_method=rsync
# Galera Node Configuration
wsrep_node_address="10.0.0.10"
wsrep_node_name="mariadb1"

Change the IP addresses highlighted above in bold with appropriate IP addresses for your network. Copy over the this file to other nodes and change the wsrep_node_address with the node’s IP address. You also need to copy Debian “maintenance” configuration (/etc/mysql/debian.cnf) from the first node to all the others, so that the credentials for the special debian-sys-maint user are the same on all nodes. Generally, if you’ve provisioned the VMs from the same VHD then there the debian.cnf file should ideally be the same across but no harm in making sure that they are the same. Now, time to create a new cluster on node 1:
$ sudo service mysql start --wsrep-new-cluster
Once the instance is up and running, start Mariadb on other nodes normally:
$ sudo service mysql start
Voila, the mariaDB cluster is up and running, you can test it by creating a temp database on node 1, inserting data into it and selecting against the DB on node 2.

Set up WordPress

Now that we have MariaDB clustering up and running, it’s time to setup WordPress on node 1 as you would normally on a LEMP stack. For now, you can edit the wp-config to point to localhost DB. Also in Azure portal, add a new endpoint for VM 1 on port 80 and add it to a Load-Balanced Set.
Complete the WP installation on Node 1, by navigating to http://yourservice.cloudapp.net/wp-admin/install.php.
Once WP is up and running on Node 1, it’s time to install it on Node 2. Follow the same steps as Node 1 except don’t create a Database & DB user on Node 2 (it would have already been synced from Node 1). Configure endpoint for Node 2 in portal, select HTTP and add it to existing load-balanced set created while configuring Node 1. There you have it, the web servers (nginx) are now load-balanced but they only talk to their local MariaDB. Now it’s time to make both Nodes’ webservers to talk to both Nodes’ MariaDB.

HyperDB

We will use HyperDB plugin for connecting to arbitary number of read/write DB servers (in this case 2), so let’s install it by downloading it and changing the dp-config.php:

$ wget http://downloads.wordpress.org/plugin/hyperdb.zip
$ unzip hyperdb.zip

You can follow the steps for installing HyperDB here: https://wordpress.org/plugins/hyperdb/installation/.
One final thing that you would have to do is to grant the WordPress user access from remote host on both nodes’ MariaDB. You can grant privileges by firing up a mysql shell on both servers:

$ mysql -u root -p
$ create user 'wordpressuser'@'10.0.0.10' identified by 'password';
$ grant SELECT,DELETE,INSERT,UPDATE ON wordpress.* TO 'wordpressuser'@'10.0.0.10';
$ flush privileges

Where:
wordpressuser: name of the user used in wp-config.php
wordpress: DB name of wordpress
10.0.0.10: IP Address of the other node, so if you are on Node 1, this would be the IP address of Node 2 and vice versa. You should now be able to connect to Node 2 MariaDB from Node 1:
mysql -u wordpressuser -p -h10.0.0.10

Handling uploads

Now that we have a clustered environment, we need to take care of syncing uploads on one node with another, we can perhaps use rsync to sync the upload folders across all nodes but given that we are hosted on Azure, let’s make use of Azure Storage for storing all the uploads, that way the uploaded media files are available across all nodes. All that needs to be done to integrate Azure Storage with WP is to install the Windows Azure Storage Plugin and go through the documentation on how to link it to your Azure Storage account. There is one issue with this plugin though, it does not (or provides the ability to) set the cache control property of the uploaded blob, which means that the files that are served via Storage have no max-age header. A quick hack is to update the windows-azure-storage-util.php and add the following line in putBlockBlob() method before the $createBlobOptions->setMetadata($metadata); line:
$createBlobOptions->setCacheControl('public,max-age=31536000');

Testing the cluster

Stop nginx on Node 1 and MariaDB on Node 2, this will ensure that Azure LB will route the web request to Node 2 since Node 1 is down, and on Node 2; HyperDB will make a call to Node 1’s MariaDB since Node 2’s DB is down.

Caveats

Given that WP is running on multiple servers, special care needs to be taken to while updating WP or adding new plugins and themes as the servers could easily get out of sync. You can either turn off WordPress’ auto-update feature and manually add any themes/plugins by copying on both servers or setup rsync between the two servers. Ideally, if you are going the rsync route, you would want one server to behave as a master (by routing all wp-admin requests to that servers) and then just setup one way sync between servers using rsync and ionotify. Unfortunately, Azure LB doesn’t support url pattern based routing so you would have to setup two-way sync which makes syncing a bit more involved.

Well that’s pretty much it on how to set up WordPress with MariaDB on Azure VM cluster.

Top