Ultimate Guide for Wordpress High-Availability on AWS: Part 1

Published May 17, 2017
Ultimate Guide for Wordpress High-Availability on AWS: Part 1


Today I want to show you another way to leverage Ansible playbooks to streamline your Wordpress deployments on Amazon Web Services.

Ansible makes it easy to deploy high-availability Wordpress sites in just a few minutes. This playbook makes having an automated, repeatable, and highly reliable Wordpress deployment process a snap!

By the time you finish this tutorial, you will have the knowledge and the code you need to start deploying Wordpress on AWS using Ansible. I will show you how to bake idempotency into your playbooks, how to take advantage of the dynamic inventory, how to modularize your playbooks with roles, and how to bring it all together into one killer, automated solution that you can reuse or repurpose.

There are two ways you can go about this tutorial.

  1. You can follow every step in the tutorial, learn how the playbook is put together, and gain insight into the thinking behind it all. If you choose to do this, make sure that you have plenty of time to follow this tutorial. You should need 2 to 3 hours or longer from start to finish, depending on your experience level. Or, if you’re already familiar with Ansible and don't need to go through the tutorial, you can simply...

  2. Download the playbook from my GitHub repo and start deploying Wordpress immediately. Your choice! The GitHub repo will be included at the very end of this tutorial.

The Toolkit


Wordpress is perhaps the most popular blogging platform on the web today. It is free, relatively easy to install and use, runs well on marginal hardware, and has tons and tons of plugins available - both free and paid - that extend its functionality and usability far beyond what the original designers had in mind. Wordpress also allows developers to completely transform its HTML output through the use of themes. Anyone can create their own theme, and there are plenty, both free and paid, that can easily be installed and customized to fit your needs. It is no wonder that so many users and developers have flocked to the platform over the years and made it what it is today.

Amazon Web Services

Amazon Web Services is the de facto standard for hosting highly-available applications on the cloud. AWS offers everything anyone could ever want in terms of virtual network infrastructure, computing power, storage capacity, content delivery, application security, database storage, analytics, mobile services, monitoring, testing, security, and much more. AWS continues to add new features every day and the breakneck pace at which it does shows no signs of relenting anytime soon.

Amazon has built an impressive platform that is simply second to none. In fact, you could say that Amazon Web Services invented cloud services. AWS has a presence in almost every major continent. The metropolitan areas hosting Amazon Web Services datacenters are called 'regions' and always have at least three distinct datacenters that AWS likes to call 'availability zones'.

While we could just as well deploy our solution across regions, to keep things simple we will focus on deploying Wordpress in a single region across multiple availability zones. There is nothing that says you cannot modify the playbook to do multi-region deployments, but that is not within the scope of this tutorial.


Ansible is a simple and powerful provisioning and configuration management tool. It requires no agents to be installed on the machines that it manages and instead works by directly connecting to to them and issuing commands via SSH or PowerShell. Yes, Ansible works with Windows too.

Its simple, readable playbook format, an actively engaged development team, a passionate community of users, and sponsorship from a major operating system vendor (Ansible was recently acquired by Red Hat, the guys behind Red Hat Enterprise Linux, CentOS, and Fedora) have helped Ansible become a staple in almost every DevOps engineer's toolkit.

Oh, and the project is free and open source!

Wordpress High-Availability

The advent of the cloud has made highly-available application architecture ubiquitous, and for good reason: a highly-available application design will allow your users to continue using your site even if one of the datacenters that hosts it completely goes offline. (And we all know that Amazon Web Services does go offline every now and then, right?)

In order to have a highly-available deployment, we must have a 'stateless' server architecture in place. This means that we will need to separate the storage and database components from our web application server. A stateless server architecture will also allow us to implement cool features like AutoScaling to deal with demand spikes. Without a stateless server, it would be next to impossible to reliably shrink and grow our deployment automatically.

Please take a moment to understand the relationship of the solution components as pictured in the diagram.

Setting Up


In order to successfully follow this tutorial, you will need to have the following:

  • A Control Host. This is the machine where Ansible will run. More often than not, this is your own workstation, but it can also be a virtual machine running on top of your workstation or somewhere else, or it can also be a bastion server. At minimum, you should be able to log on to this control host at its console or remotely via SSH and also have root access or be in the sudoers group. The control host will need the following software packages installed:
    • AWS CLI.
    • Python 2.7 or better, pip, and the boto and boto3 libraries.
    • Ansible. Preferably installed using pip but can also be installed from
      source. Please refrain from installing Ansible from your distro's package
      manager as these packages are typically outdated.
    • An IAM user with permissions to create and manage resources on IAM, EC2, RDS, S3, EFS, Cloud Front, and Certificate Manager. This user should be able to access AWS programmatically and you should have its AKID and secret.
    • A user that can log on to the AWS console. This user can be the same user
      as above.
    • You should also be familiar with the command line and should know how to perform all basic system administration tasks such as creating, moving, copying, and deleting files; adding and removing users to and from the system; installing software packages, etc.

Before we get to our playbook, let's create a new folder where we will create our solution. On your desktop (or any other location that makes sense to you) create a new folder. I will call my folder ansible-aws-wordpress-ha but you can certainly name it anything you want. Descend into the folder and create the following directory structure:

   + -- inventory
   |      + -- aws_keys.sh
   + -- roles
   |      + -- role_template
   |                + -- defaults
   |                |        + -- main.yml
   |                + -- handlers
   |                + -- meta
   |                | .    + -- main.yml
   |                + -- tasks
   |                       + -- main.yml
   + -- tasks
   |      + --- main.yml
   + -- vars
   |      + -- main.yml
   + -- vault
   | .    + -- aws_keys.yml
   + -- ansible.cfg
   + -- ansible-aws-wordpress-ha.yml

All of the .yml and .cfg files above are empty text files, for now.

Ansible's Configuration File

We can modify the way in which Ansible works by means of its configuration file: ansible.cfg. In an Ansible playbook, you are not required to include this file. When the file is absent, Ansible will take its configuration from the default ansible.cfg located in the /etc/ansible folder. However, the default configuration will not work for us here, so we will need to create our own ansible.cfg to override certain defaults. Open the ansible.cfg file created above in your favorite text editor and enter the following:

ansible_managed = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}
hostfile = inventory/ec2.py
remote_user = ubuntu # Use 'ec2-user' instead if you want to deploy to a Red Hat-based EC2 instance.

pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=30m -o StrictHostKeyChecking=no
control_path = /tmp/ansible-ssh-%%h-%%p-%%r

Here we are changing the default value of the ansible_managed variable, which we will use later in our EC2 tags. We are also telling Ansible to use a specific script to manage our inventory and we will get to that in a moment. We also tell Ansible to use the user named 'ubuntu' when making remote connections to the instances that we will be launching.

I prefer to use Ubuntu on AWS because, in my experience, it is the most performant of all Linux distros available. You are under no obligation to use Ubuntu. As a matter of fact, if you have Red Hat-based instances deployed (such as Amazon Linux, CentOS, Fedora, or Suse), you should probably stick to that for the sake of management and familiarity with the operating system. This playbook will work for both Debian and Red Hat-based distributions. The only distinction you need to make is in the variable 'remote_user' in the ansible.cfg file. If you are using a Red Hat-based distribution, make sure to change the remote_user value from 'ubuntu' to 'ec2-user'.

Dynamic Inventory

Standard Ansible practice has been to maintain <hostname>.yml and/or <groupname>.yml files in the inventory folder. In these files we put variables that apply only to those hosts and/or groups. There is nothing wrong with this approach if you are managing physical hosts. However, when dealing with the cloud, it is recommended to use the dynamic inventory scripts included with Ansible as you are more likely to be managing hundreds if not thousands of virtual machines, making static inventory files a pain to use. Ansible's dynamic inventory scripts are constantly being improved, and features get added on a regular basis. If you installed Ansible using pip, it will be easy to keep Ansible up-to-date with the latest upstream changes.

Currently, Ansible has dynamic inventory scripts available for AWS, Azure, Digital Ocean, OpenStack and more. Today we are working with AWS and this means we will only concern ourselves with the AWS dynamic inventory scripts. Find the ec2.ini and ec2.py files and copy them over to our playbook's folder, In a standard Ansible installation, you will find these files in the /etc/ansible folder. Don't move or make changes to those files, just copy them to the empty 'inventory' folder you created earlier inside your playbook's folder. Remember, you should not need to modify the original files in any way.


There are two ways to tackle the task of configuring your AWS key and secret for the purposes of this tutorial. The first option is to set both the key and secret as environment variables. In fact, if you are an AWS veteran, you may already be doing this in your bash.rc file and that means you can skip ahead to the next section.

The second option, and the one I recommend, is to use Ansible Vault. Reach for your favorite text editor and open the ./vault/aws_keys.yml file in your playbook's folder for editing and enter the following text:

# AWS Keys - needed in order to run this playbook successfully
aws_access_key = ==REMOVED==
aws_secret_key = ==REMOVED==

Obviously, you should substitute your own values where appropriate. And make sure your values are enclosed in quotes. Save and close the file. Normally, We would encrypt the file after modifying it, but we are going to wait until later to do so.


Roles are one of Ansible's most powerful concepts. Think of a role as a sort of playbook within a playbook. Roles allow you to compartmentalize your configuration management and provisioning tasks and enable you to reuse those tasks (as roles) in your playbooks. And reusability is one of our goals. So we will definitely use roles.

The first thing you need to do when creating your roles is to think of all of the configuration and provisioning you are doing and break these processes down into their basic tasks. If you were deploying a LAMP server, for example, you know that you need to deploy the server, and then install Apache, MySQL, and PHP. So you could break down the process into those 4 components: deploy server, install Apache, install MySQL, install PHP. Then run additional tasks to configure those components. Each of these steps would be encapsulated as a single role in Ansible, allowing you to reuse those roles individually in other playbooks.

With this in mind, let's now think of all of the things we need to do in order to set up our Wordpress environment. Refer to our diagram if you need to. We will need to perform all of the following:

  • Create VPC
  • Create Subnets
  • Create Security Groups
  • Create Internet Gateway
  • Create Routing Tables
  • Generate Keypair
  • Launch RDS Cluster
  • Create EFS Filesystem
  • Launch EC2 instances
  • Create Route53 Domain (Optional)
  • Generate SSL Certificate
  • Launch Elastic Load Balancer
  • Create AutoScaling Configuration
  • Deploy Wordpress

This is quite a bit of work. Imagine having to do that by hand every time you needed to deploy a Wordpress site on AWS. Ansible automates the entire process.

Copying the Role Template folder

Descend into the roles folder you created earlier and copy the role_template folder a few times making sure to rename each copy until you have all of the following folders under the roles folder:

  • mrjvazquez-aws-ha-vpc
  • mrjvazquez-aws-ha-vpc-subnets
  • mrjvazquez-aws-ha-vpc-routing-table
  • mrjvazquez-aws-ha-vpc-igw
  • mrjvazquez-aws-generate-keypair
  • mrjvazquez-aws-ha-security-groups
  • mrjvazquez-aws-ha-rds-cluster
  • mrjvazquez-aws-ha-efs
  • mrjvazquez-aws-ha-ec2
  • mrjvazquez-aws-route53-domain
  • mrjvazquez-aws-ssl-certificate
  • mrjvazquez-aws-ha-elb
  • mrjvazquez-aws-ha-autoscaling

When you are done, you will have the 13 folders above, each with a defaults, handlers, meta, and tasks folder and an empty main,yml file under it. You can then delete the role_template folder as you will no longer be needing it. If you are paying attention, you will notice that we did not create a role for deploying Wordpress. This is because we will deploy Wordpress from the main playbook file later on. Don't worry, I have not forgotten anything.

Creating our VPC

Descend into the ./roles/mrjvazquez-aws-ha-vpc folder and open the file ./tasks/main.yml in your favorite text editor and enter the following text. Since we are writing YML, spacing is very important. Never use tabs.

# Role for creating an AWS VPC
name: Creating your AWS VPC
- ec2_vpc_net:
    name: "{{ mrjvazquez_aws_vpc_name }}"
    aws_access_key: "{{ aws_access_key }}" # Remove if using AWS env vars
    aws_secret_key: "{{ aws_secret_key }}" # Remove if using AWS env vars
    cidr_block: "{{ mrjvazquez_aws_vpc_cidr_block }}"
    region: "{{ mrjvazquez_aws_vpc_region }}"
    tags: "{{ mrjvazquez_aws_vpc_tags }}"
    tenancy: "{{ mrjvazquez_aws_vpc_tenancy }}"
  register: aws_vpc

Save and close the file. Now open the file in ./defaults/main.yml and enter the following:

# These are our role default values.  These are just starting values, we will
# override them later in our playbook.
mrjvazquez_aws_vpc_name = "MyVPC"
mrjvazquez_aws_vpc_cidr_block = ""
mrjvazquez_aws_vpc_region = "us-west-1"
mrjvazquez_aws_vpc_tags ='
  name: "{{ mrjvazquez_aws_vpc_name }}"
  managed: "{{ ansible_managed }}"
  other: "{{ copyright }}"'
mrjvazquez_aws_vpc_tenancy = "default"

In the first block of code above, we tell Ansible we want it to use the ec2_vpc_net module to create our VPC. Any values enclosed in curly brackets {{ like this }} are variable references. This means that the value of the variable will be inserted there. We assigned some default variable values in the main.yml file with the second block of code. These 'default' values can be easily replaced later on. Finally, we register the result of our actions in a variable called aws_vpc which we will use in subsequent roles.

Finally, we will modify the meta/main.yml file by entering the following text:

Since this role has no handlers, you can delete the handlers folder if so desired. It is quite OK if you leave the empty folder in place.

VPC Subnets

We now have our VPC but we will need some subnets before we can do anything with it. So let's move over to the mrjvazquez-aws-ha-vpc-subnets role folder and open the ./tasks/main.yml file in, you've guessed it, your favorite text editor. Enter the text below:

# Role to configure 2 Private and 2 Public Subnets in a VPC
# Will create 1 subnet in each AWS availability zone
- name: Creating Private Subnets
    aws_access_key: "{{ aws_access_key }}" #Remove if using AWS env vars
    aws_secret_key: "{{ aws_secret_key }}" #Remove if using AWS env vars
    state: "{{ mrjvazquez_aws_vpc_private1_az_state }}"
    vpc_id: "{{ aws_vpc.vpc_id }}
    az: "{{ mrjvazquez_aws_vpc_private1_az }}"
    cidr: "{{ mrjvazquez_aws_vpc_private1_cidr }}"
      "{{ mrjvazquez_aws_vpc_private_subnet_tags }}"
  register: aws_vpc_private1_subnet
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    state: "{{ mrjvazquez_aws_vpc_private1_az_state }}"
    vpc_id: "{{ aws_vpc.vpc_id }}
    az: "{{ mrjvazquez_aws_vpc_private2_az }}"
    cidr: "{{ mrjvazquez_aws_vpc_private2_cidr }}"
      "{{ mrjvazquez_aws_vpc_private2_subnet_tags }}"
  register: aws_vpc_private2_subnet

- name: Creating Public Subnets
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    state: "{{ mrjvazquez_aws_vpc_private1_az_state }}"
    vpc_id: "{{ aws_vpc.vpc_id }}
    az: "{{ mrjvazquez_aws_vpc_public1_az }}"
    cidr: "{{ mrjvazquez_aws_vpc_public1_cidr }}"
      "{{ mrjvazquez_aws_vpc_public1_subnet_tags }}"
  register: aws_vpc_public1_subnet
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    state: "{{ mrjvazquez_aws_vpc_private1_az_state }}"
    vpc_id: "{{ aws_vpc.vpc_id }}
    az: "{{ mrjvazquez_aws_vpc_public2_az }}"
    cidr: "{{ mrjvazquez_aws_vpc_public2_cidr }}"
      "{{ mrjvazquez_aws_vpc_public2_subnet_tags }}"
  register: aws_vpc_public2_subnet

Now save the file and close it and open ./defaults/main.yml and enter the following text:

# These are our role default values.  These are just starting values, we will
# override them later in our playbook.
mrjvazquez_aws_vpc_private1_az_state = "present" # We want to create a subnet
mrjvazquez_aws_vpc_private1_az = "{{ mrjvazquez_aws_vpc_region }}a"
mrjvazquez_aws_vpc_private1_cidr = ""
mrjvazquez_aws_vpc_private1_subnet_tags = ""
mrjvazquez_aws_vpc_private2_az_state = "present"
mrjvazquez_aws_vpc_private2_az = "{{ mrjvazquez_aws_vpc_region }}b"
mrjvazquez_aws_vpc_private2_cidr = ""
mrjvazquez_aws_vpc_private2_subnet_tags = ""
mrjvazquez_aws_vpc_public1_az_state = "present"
mrjvazquez_aws_vpc_public1_az = "{{ mrjvazquez_aws_vpc_region }}c"
mrjvazquez_aws_vpc_public1_cidr = ""
mrjvazquez_aws_vpc_public1_subnet_tags = ""
mrjvazquez_aws_vpc_public2_az_state = "present"
mrjvazquez_aws_vpc_public2_az = "{{ mrjvazquez_aws_vpc_region }}d"
mrjvazquez_aws_vpc_public2_cidr = ""
mrjvazquez_aws_vpc_public2_subnet_tags = ""

We have just used the ec2_vpc_subnet module four times to set up our four subnets and again, we placed our default starting values into a separate file that we can easily override later. We also stored the result of each module run into a separate variable we can use later in subsequent roles. Are you starting to see a pattern here?

There is another very important concept to be gained from the example above. First, you may have noticed that we used the variable {{ aws_vpc.vpc_id }}. If you are familiar with dot notation, you will understand this to mean "the value of the 'vpc_id' property of the 'aws_vpc' object". But where did this 'aws_vpc' object come from? We did not define it in this role. Well, it came from our previous role! We defined the variable in the very last step like so:

register: aws_vpc

The 'register' command is used by Ansible to store factual information about the environment. That is the reason why Ansible calls these bits of information 'facts'. Ansible facts can be accessed by any task, role, handler, or play in the current playbook.

The other important concept is that of variable concatenation. This concept is illustrated by the following line in our defaults/main.yml file:

mrjvazquez_aws_vpc_public1_az = "{{ mrjvazquez_aws_vpc_region }}c"

That is, we stitch together two values to generate a completely different value. In this case, we concatenate the {{ mrjvazquez_aws_vpc_region }} value and the letter 'c' to arrive at "us-west-1c", which is what {{ mrjvazquez_aws_vpc_public1_az }} will be set to. Pretty cool right?


We will stop right here and will continue our tutorial next week in part 2 of 3. Stay tuned! If you have any questions, feel free to drop me a line at mrjvazquez@gmail.com or find me on Skype as mrjvazquez.

Discover and read more posts from Jorge Vazquez
get started
Enjoy this post?

Leave a like and comment for Jorge

David Sapp
3 months ago

The portion of this guide that says “Finally, we will modify the meta/main.yml file by entering the following text:” has no code underneath. Also, are parts 2 and 3 complete?

Get curated posts in your inbox

Read more posts to become a better developer