This tutorial walks through setting up AWS infrastructure for WordPress, starting at creating an AWS account. We'll manually provision a single EC2 instance (i.e an AWS virtual machine) to run WordPress using Nginx, PHP-FPM, and MySQL.
This tutorial assumes you're relatively comfortable on the command line and editing system configuration files. It is intended for folks who want a high-level of control and understanding of their infrastructure. It will take about half an hour if you don't Google away at some point.
If you experience any difficulties or have any feedback, leave a comment. 🐬
Coming soon: I'll write another tutorial on a high availability setup for WordPress on AWS, including load-balancing multiple application servers in an auto-scaling group and utilizing RDS.
Amazon Web Services (AWS) offers cloud computing services, including everything necessary to run a WordPress site. This is similar to a web hosting company service, with a few differences:
- You have a high-level of control over the infrastructure. e.g. You can edit your
php.ini
file as well as setting up a load balancer to distribute load to multiple application servers. - Server resources are easy to provision and resize. Need to increase a server's RAM from 1GB to 30GB? Not a problem.
- You only pay for the resources you use. Don't lock yourself in to a high priced plan for more resources that you might not need.
Sign up for an AWS account. The Free Pricing Tier includes a year of AWS usage to a basic set of server resources for free.
Log into the AWS console, a web-based administrative interface for AWS.
IAM (Identity and Access Management) is the AWS service for managing user access to other services and server resources.
When you log into the AWS web console with your main Amazon account, you're using AWS in root user mode, which is fine for our purposes.
In order to use the AWS command line interface in this tutorial, we'll create an IAM user rather than using the root user's credentials.
From the AWS Console homepage, under Administration & Security, click Identity & Access Management to go to the IAM dashboard.
Click Users to see a list of all registered IAM users for the AWS account, which should be empty. Click Create New Users, and create a user account for yourself and download the access key details. You'll use access key credentials to authenticate your computer with AWS.
An IAM Group defines a set of permissions. Users are assigned to groups, granting the user permissions.
While still in the IAM console, click Groups to see a list of all registered IAM Groups for the AWS account, which should be empty. Create a new IAM Group called "Administrators". Attach the AdministratorAccess policy to the group, which provides full access to all services.
Return to the Users list and add your user account to the "Administrators" group.
The AWS command line interface is a unified tool for managing services from your computer's terminal. Most AWS management tasks (e.g. creating a new EC2 instance) can be performed in the web console or on the command line. Which to use for any task will depend on workflow preferences.
Install the AWS command line interface for your computer's terminal.
Run aws configure
, and supply the access key credentials for the IAM user created earlier. This authenticates your computer under that user account.
Set a default AWS region during this configuration. AWS regions are different data centers around the world you can choose to host resources. Your default is probably "us-west-2"; check the region dropdown in the menu bar in the web console to confirm this.
An EC2 key pair is a cryptographic public and private key pair required to authorize ssh access to an EC2 instance. A key pair is tied to an IAM user. To authorize access to an EC instance, a private key exists on your computer which matches a public key associated with the instance.
If the folder ~/.ssh
doesn't exist on your computer, create it. This is where ssh credential files are stored.
mkdir ~/.ssh
Create a key pair with the AWS CLI. Replace {{KEY_NAME}} with a key name and private key filename. I called mine "aws-eric".
aws ec2 create-key-pair --key-name {{KEY_NAME}} --query 'KeyMaterial' --output text > ~/.ssh/{{KEY_NAME}}.pem
This creates a new private key file in the ~/.ssh
directory.
Change the file permissions for the private key so that only your user can read it.
chmod 400 ~/.ssh/{{KEY_NAME}}.pem
EC2 (Elastic Compute Cloud) is the service for managing generic-use virtual machines called EC2 instances. We'll create an EC2 instance to run as the WordPress web server.
From the AWS console homepage, click EC2 to enter the EC2 console. Click Instances to see a list of all EC2 instances for the account, which should be empty.
Click Launch Instance.
In "Step 1: Choose an AMI", we need to choose a disk image to launch an operating system onto the instance. Select the "Amazon Linux AMI (HVM)".
In "Step 2: Choose Instance Type", select the "t2.micro".
Continue through the wizard using the defaults.
In "Step 6: Configure Security Group", create a new Security Group called "WordPressApplicationServer". A Security Group is a firewall that limits traffic to an instance. Add a rule to allow SSH access, and under the Source column, only allow access from your IP. Add a rule to allow HTTP access from anywhere. Add a rule to allow HTTPS access from anywhere.
Review and Launch the instance. When prompted, choose the key pair created previously to authorize access to the instance. Booting will take a moment. Return to the instance list table to check its state.
From the instance list table, click the newly launched instance to open a details pane. Using the Public DNS (i.e. the hostname) listed here, login via ssh in a terminal.
ssh ec2-user@{{INSTANCE_PUBLIC_HOSTNAME}} -i ~/.ssh/{{PRIVATE_KEY_FILE_NAME}}
The default system user account for Amazon Linux is "ec2-user," which has sudo access.
Update all system packages that may be out of date in the distribution.
sudo yum update
The directory root for the site which Nginx serve files from will live in
/sites/{{SITE_DOMAIN}}.com/public
. Create the folder.
sudo mkdir -p /sites/{{SITE_DOMAIN}}.com/public
Download the latest stable WordPress version into the folder.
cd /sites/{{SITE_DOMAIN}}.com/public
sudo wget https://wordpress.org/latest.tar.gz
sudo tar zxf latest.tar.gz
cd wordpress
sudo cp -rpf * ../
cd ../
sudo rm -rf wordpress/ latest.tar.gz
Nginx is a high-performance web server and reverse proxy.
sudo yum install nginx
Nginx installs with a basic configuration in /etc/nginx/
. Overwrite this with more pragmatic defaults from HTML5 Boilerplate's nginx config.
We'll use git to checkout the H5BP nginx config git repository, so we'll install git to do that.
sudo yum install git
cd /etc
sudo mv nginx nginx-previous
sudo git clone https://github.com/h5bp/server-configs-nginx.git nginx
Nginx creates a system user "nginx" to run the web server process. Update the nginx.conf
file appropriately.
cd /etc/nginx
sudo vi nginx.conf
# Run as a less privileged user for security reasons.
user nginx nginx;
Change ownership of the site directory and its entire contents to the "nginx" system user.
sudo chown -R nginx:nginx /sites/{{SITE_DOMAIN}}.com/public
H5BP's Nginx configuration is langauge-agnostic — it doesn't include configuration
for running scripts for any programming languages. Copy the fastcgi_params
file from the default nginx configuration
which we'll need to run a site via PHP-FPM.
sudo cp /etc/nginx-previous/fastcgi_params /etc/nginx
Create the folder for where Nginx logs will be written.
sudo mkdir -p /usr/share/nginx/logs/
Nginx configuration needs to be written to route requests for the domain appropriately for the WordPress site. Inside the sites-available
folder, create a new configuration file.
sudo vi /etc/nginx/sites-available/{{SITE_DOMAIN}}.com
Use the boilerplate below for the configuration. Replace {{SITE_DOMAIN}}
with your site's domain name.
# Define the microcache path.
fastcgi_cache_path /etc/nginx/cache levels=1:2 keys_zone=microcache:100m inactive=60m;
# Redirect http traffic to https.
server {
listen [::]:80;
listen 80;
server_name {{SITE_DOMAIN}}.com;
return 301 https://{{SITE_DOMAIN}}.com$request_uri;
}
server {
listen 443 ssl;
server_name {{SITE_DOMAIN}}.com;
# Include defaults for allowed SSL/TLS protocols and handshake caches.
include h5bp/directive-only/ssl.conf;
# config to enable HSTS(HTTP Strict Transport Security) https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security
# to avoid ssl stripping https://en.wikipedia.org/wiki/SSL_stripping#SSL_stripping
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
ssl_certificate_key /etc/sslmate/{{SITE_DOMAIN}}.com.key;
ssl_certificate /etc/sslmate/{{SITE_DOMAIN}}.com.chained.crt;
# Path for static files
root /sites/{{SITE_DOMAIN}}.com/public;
#Specify a charset
charset utf-8;
# Include the basic h5bp config set
include h5bp/basic.conf;
location / {
index index.php;
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_cache microcache;
fastcgi_cache_key $scheme$host$request_method$request_uri;
fastcgi_cache_valid 200 304 10m;
fastcgi_cache_use_stale updating;
fastcgi_max_temp_file_size 1M;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Local variables to track whether to serve a microcached page or not.
set $no_cache_set 0;
set $no_cache_get 0;
# If a request comes in with a X-Nginx-Cache-Purge: 1 header, do not grab from cache
# But note that we will still store to cache
# We use this to proactively update items in the cache!
if ( $http_x_nginx_cache_purge ) {
set $no_cache_get 1;
}
# If the user has a user logged-in cookie, circumvent the microcache.
if ( $http_cookie ~* "comment_author_|wordpress_(?!test_cookie)|wp-postpass_" ) {
set $no_cache_set 1;
set $no_cache_get 1;
}
# fastcgi_no_cache means "Do not store this proxy response in the cache"
fastcgi_no_cache $no_cache_set;
# fastcgi_cache_bypass means "Do not look in the cache for this request"
fastcgi_cache_bypass $no_cache_get;
}
}
Create a symlink in the sites-enabled folder to enable the site.
sudo ln -s /etc/nginx/sites-available/{{SITE_DOMAIN}}.com /etc/nginx/sites-enabled/{{SITE_DOMAIN}}.com
Always start Nginx when the system boots.
sudo chkconfig nginx on
We haven't started the Nginx web server, and won't just yet. An SSL certificate for the site's domain needs to be in place first, otherwise we'll encounter a fatal Nginx error when starting Nginx.
AWS is a domain registrar. Route 53 is the service for domain registration and DNS management.
From the AWS Console homepage, under Networking, click Route 53 to go to the Route 53 dashboard.
In the navigation menu, click Registered Domains to see the list of all domains registered for the AWS account, which should be empty. Click the Register Domain, and follow through the wizard to register a new domain.
The domain should have DNS configured to point traffic to the EC2 instance. IP addresses of EC2 instances can change, so we can't create an A record to point the domain directly at the instance. We'll create an Elastic IP address, which we can set our domain name to resolve to, which in turn will direct traffic to the EC2 instance.
In the EC2 Console, Click Elastic IPs to see a list of all allocated Elastic IPs, which should be empty. Click Allocate New Address and create one. Select the new IP and click Associate Address. Select the instance created earlier to associate the IP with.
A Hosted Zone is a group of DNS records which define how the domain name will work. In the Route 53 console, click Hosted Zones to see all hosted zones, which should be empty. Click the Create Hosted Zone button. Enter the site's domain name and click Create.
Now in the context of the new hosted zone, click Create Record Set. Ensure the type is "A" for A Record. In the "Value" field, enter the Elastic IP Address created previously. This resolves traffic to the domain Elastic IP and thereby the EC2 instance.
If traffic comes in expecting the site under www.{{SITE_DOMAIN}}.com, this traffic should be accepted
and redirected to a non-"www" version of the URL. Click Create a Record Set. Set the "name" to www.{{SITE_DOMAIN}}.COM
. Set the "type" to "CNAME". Set "Value" to {{SITE_DOMAIN}}.COM}
and save the record set. We'll handle the redirect at the nginx routing layer.
Severing your site over HTTPS is an absolute necessity to guarantee your users a basic amount of confidentiality and authenticity.
In the Nginx configuration for the site we created, the server is listening for traffic
over the HTTPS port (443). We also stipulated the location of SSL certificates
(e.g. /etc/sslmate/{{SITE_DOMAIN}}.com.chained.crt
). This assumes we'll register
an SSL certificate with sslmate, which we'll do now.
Go to sslmate and register for an account.
The SSL certificate and key need to live on the EC2 instance. While SSHed into the instance, install the sslmate command line utility.
sudo wget -P /etc/yum.repos.d https://sslmate.com/yum/centos/SSLMate.repo
sudo wget -P /etc/pki/rpm-gpg https://sslmate.com/yum/centos/RPM-GPG-KEY-SSLMate
sudo yum install sslmate
Buy an SSL certificate for the domain.
sudo sslmate buy {{SITE_DOMAIN}}.com
sslmate will email the contact of your choosing associated with the domain name. Open the email and follow the authorization link.
The sslmate program will remain open until the authorization email is confirmed. After it does, it will alert you of the key and certificate files it created.
Waiting for ownership confirmation...
Your certificate is ready for use!
Private key: /etc/sslmate/{{SITE_DOMAIN}}.com.key
Certificate: /etc/sslmate/{{SITE_DOMAIN}}.com.crt
Certificate chain: /etc/sslmate/{{SITE_DOMAIN}}.com.chain.crt
Certificate with chain: /etc/sslmate/{{SITE_DOMAIN}}.com.chained.crt
```
## Start Nginx
Now that the SSL certificates exist, start Nginx.
```bash
sudo service nginx start
```
## Install PHP
We'll run PHP with the [PHP-FPM package](http://php-fpm.org/).
```bash
sudo yum install php-fpm php-mysql
```
The PHP-FPM configuration assumes the user invoking it will be "apache". Replace "apache" with "nginx" in the configuration file.
```bash
sudo vi /etc/php-fpm.d/www.conf
```
```aconf
; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
; will be used.
; RPM: apache Choosed to be able to access some dir as httpd
user = nginx
; RPM: Keep a group allowed to write in log dir.
group = nginx
```
Start the PHP-FPM server.
```bash
sudo service php-fpm start
```
THe PHP-FPM server should always be on — tell the system to initialize it on startup.
```bash
sudo chkconfig php-fpm on
```
## Install and configure MySQL
Install the MySQL system package.
```bash
sudo yum install mysql-server
```
Start the MySQL daemon.
```bash
sudo service mysqld start
```
Always start the MySQL daemon when the system boots.
```bash
sudo chkconfig mysqld on
```
Run the MySQL secure installation to establish basic security settings.
```bash
sudo /usr/bin/mysql_secure_installation
```
Enter a secure root password. Remove anonymous users. Disable root login remotely. Remove test database and access to it. Reload privilege tables now.
Log into the MySQL interactive shell, create a database and a MySQL user with limited privileges. Replace `{{WP_DATABASE_NAME}}`, `{{WP_DATABASE_USER}}`, and `{{WP_DATABASE_USER_PASSWORD}}` with your preferred credentials.
```bash
mysql -u root -p
CREATE DATABASE {{WP_DATABASE_NAME}};
CREATE USER '{{WP_DATABASE_USER}}'@'localhost' IDENTIFIED BY '{{WP_DATABASE_USER_PASSWORD}}';
GRANT ALL ON {{WP_DATABASE_NAME}}.* TO '{{WP_DATABASE_USER}}'@'localhost';
exit;
```
## Configure WordPress via the web installer
Visit your site in the browser, which will load the WordPress five minute install screen. Configure the install with the database credentials created earlier.
## Follow-up
At this point the WordPress install is running. There are a few other administrative activities to fine-tune the install.
## Install Memcached
[Memcached](http://memcached.org/) is an object caching system which can be used to speed up PHP script processing by avoiding expensive database queries with WordPress' Caching API. Read more about [Memcached and WordPress](http://scotty-t.com/2012/01/20/wordpress-memcached/).
Install the Memcached system package.
```bash
yum install memcached
```
Always start Memcached when the system boots.
```bash
sudo chkconfig memcached on
```
Start Memcached.
```bash
sudo service memcached start
```
Install the PHP Memcache package.
```bash
sudo yum install php-pear php-pecl-memcache
```
Restart Nginx and PHP-FPM.
```bash
sudo service php-fpm restart
sudo service nginx restart
```
Install the [WordPress Memcached Drop-in](https://wordpress.org/plugins/memcached/installation/) by copying `object-cache.php` into the `wp-content` folder.
## Install Fail2Ban
[Fail2Ban](http://www.fail2ban.org/wiki/index.php/Main_Page) watches application logs for malicious activity and bans IP addresses from interacting with your server if any mischief is found. This will protect you from brute force attacks.
Install the Fail2ban.
```bash
sudo yum install fail2ban
```
Start Fail2Ban.
```bash
sudo service fail2ban start
```
Always start Fail2Ban when the system boots.
```bash
sudo chkconfig fail2ban on
```
Fail2Ban comes with configuration for basic services like ssh and MySQL, but not Nginx. [Configure fail2ban for Nginx](https://rtcamp.com/tutorials/nginx/fail2ban/).
## Purging Nginx microcache
The Nginx configuration we set stores full-page caches in the microcache for 10 minutes by default. If you edit a post in WordPress, that cache will not be busted automatically. In the same config, we set the cache to purge when a specific HTTP header is sent. Use [this plugin](https://github.com/staylor/scottyandallie/blob/master/wp-content/mu-plugins/cache-purge.php) to hit the cache purge endpoint whenever a post is edited.