# Setting up a WordPress site on AWS 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. ## About AWS [Amazon Web Services (AWS)](http://aws.amazon.com/) 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](http://aws.amazon.com/pricing/). Don't lock yourself in to a high priced plan for more resources that you might not need. ## Create an AWS account [Sign up for an AWS account](http://aws.amazon.com/). The [Free Pricing Tier](http://aws.amazon.com/free/) includes a year of AWS usage to a basic set of server resources for free. ## Log into the AWS console Log into the [AWS console](https://console.aws.amazon.com/console/home), a web-based administrative interface for AWS. ## Create an IAM user account [IAM (Identity and Access Management)](http://aws.amazon.com/iam/) 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. ## Install and configure the AWS command line interface The [AWS command line interface](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html) 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](http://docs.aws.amazon.com/cli/latest/userguide/installing.html). 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](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html) 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. ## Create an EC2 key pair An [EC2 key pair](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html) 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. ```bash 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". ```bash 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. ```bash chmod 400 ~/.ssh/{{KEY_NAME}}.pem ``` ## Create an EC2 instance [EC2 (Elastic Compute Cloud)](http://aws.amazon.com/ec2/) 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](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-network-security.html) 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. ## SSH into the EC2 instance 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. ```bash 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 system packages Update all system packages that may be out of date in the distribution. ```bash sudo yum update ``` ## Download WordPress The directory root for the site which Nginx serve files from will live in `/sites/{{SITE_DOMAIN}}.com/public`. Create the folder. ```bash 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 ``` ## Install and configure Nginx [Nginx](http://nginx.org/) is a high-performance web server and reverse proxy. ```bash 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](https://github.com/h5bp/server-configs-nginx). We'll use git to checkout the H5BP nginx config git repository, so we'll install git to do that. ```bash 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. ```bash cd /etc/nginx sudo vi nginx.conf ``` ```nginx # 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. ```bash 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. ```bash sudo cp /etc/nginx-previous/fastcgi_params /etc/nginx ``` Create the folder for where Nginx logs will be written. ```bash 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. ```bash 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. ```nginx # 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. ```bash sudo ln -s /etc/nginx/sites-available/{{SITE_DOMAIN}}.com /etc/nginx/sites-enabled/{{SITE_DOMAIN}}.com ``` Always start Nginx when the system boots. ```bash 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. ## Register a domain AWS is a domain registrar. [Route 53](http://aws.amazon.com/route53/) 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. ## Create an Elastic IP address 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. ## Configure the Domain to point to the Elastic IP 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. ## Create an SSL certificate 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](https://sslmate.com/), which we'll do now. Go to [sslmate](https://sslmate.com/) 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. ```bash 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. ````bash 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.