This beginner’s guide explains how to use AWS CloudFormation to make an all-in-one WordPress server. This is an absolutely crazy thing to do in production; having all your components on one box without any redundancy or load control is suicide. The purpose of this guide is to introduce CloudFormation using WordPress as an example and no more. Read on if that interests you.

Brief Introduction to AWS

Amazon Web Services is Amazon’s Infrastructure as a Service offering, providing a wide variety of networking and server components that can be managed programmatically.

The main entry point to AWS’ suite of products is the AWS Console, a web-based UI that provides users with tools to manage their AWS components. The console can be daunting at first, and the user experience leaves much to be desired, but that’s basically life with AWS. Better to accept the positive rather than dwell on the issues and inconsistencies.

I’d recommend logging in to the AWS Console now and having a look around. Notable places to check out would be:

  • Route 53: Domain and DNS service for registering and managing domains
  • S3: Highly-available and redundant file storage service
  • EC2: Virtual instances running many different linux flavors or Windows
  • CloudFormation: Stack management and the core topic of the article

Fees

All AWS products carry a fee structure that’s worth taking the time to understand. Before creating any AWS resource, review the fees for that service so you don’t go bankrupt. I kid you not, the effects can be devastating if you don’t know what you are doing. If eligible, try to stay within the free tier of each service; otherwise, use the smallest resource possible from the start. For example, just use a micro or nano instance for this tutorial, and keep it stopped when you’re not using it.

For the Tinkerer: Try Creating an EC2 Instance in the Console

While this guide does not cover the creation of EC2 instance from the AWS console, I would recommend doing it now. It will help you understand what CloudFormation will be doing below! It should be pretty self-explanatory following this official Amazon guide, but here are some other tips:

  • Use the t2.micro size for test instances because they are the cheapest.
  • You’ll need to chose an AMI, which will be the base Operating System image for your new instance. Most people use the standard Ubuntu 64-bit AMI.
  • A Security Group is basically a firewall. You’ll want to permit SSH, HTTP, and HTTPS access to your machine.
  • Save the .pem (RSA) key when AWS provides it to you! This is how you will login to your instance, and without it, you’ll have to start over (which is really more of a nuisance than a major issue at this point).

Using the AWS Command Line Tool to Make an EC2 Instance

Often it’s easier to use Amazon’s command-line tool for performing tasks like uploading files to S3, deploying CloudFormation templates, or a variety of other tasks. That’s not the case with an EC2 instance, but it’s nice to know that it can be done.

Something to note: our Amazon representative at work said that the command-line tool is generally more up-to-date than the AWS Console, so it’s a good thing to add to your AWS management arsenal!

The command-line tool is a Python program. To install it, follow these simple guides:

  1. Get an access-key and secret for your account
  2. Install the command-line tools

This pseudocode will create an EC2 instance, given you know your default VPC ID, which can be found in the EC2 area of the Console. I haven’t tested this yet, but it’s close to what you’d need to do.

$ aws ec2 create-key-pair --key-name MyKeyPair > ~/.ssh/MyKeyPair.pem
$ aws ec2 create-security-group ... # outputs SEC_GROUP_ID
$ aws ec2 authorize-security-group-ingress --group-id SEC_GROUP_ID --protocol tcp --port 22 --cidr `wget http://ipinfo.io/ip -qO -`/32
$ aws ec2 authorize-security-group-ingress --group-id SEC_GROUP_ID --protocol tcp --port 80 --cidr `wget http://ipinfo.io/ip -qO -`/32
$ aws ec2 authorize-security-group-ingress --group-id SEC_GROUP_ID --protocol tcp --port 443 --cidr `wget http://ipinfo.io/ip -qO -`/32
$ aws ec2 run-instances --image-id ami-69631053 --count 1 --instance-type t2.micro --key-name MyKeyPair --security-groups my-sg

CloudFormation

CloudFormation automates the steps above. Additionally, it allows users to group components into stacks and configure them from within one or more JSON templates. This is especially helpful for automating the creation of complex networks, where multiple environments need to be spun up for development, testing, and staging.

Here’s a bare-bones JSON template that shows the general structure of AWS CloudFormation templates:

{
    "Description" : "A skeleton template.",
    "Outputs": {
        "Output1": {
            "Description": "The first output",
            "Value": { "Ref": "Resource1" }
        }
    },
    "Parameters": {
        "Param1": {
            "Description": "The region for our S3 bucket",
            "Type": "String",
            "Default": "ap-southeast"
        }
    },
    "Resources": {
        "Resource1": {
            "Type": "AWS::EC2::...",
            "Properties": {...}
        }
    }
}

This template contains the following sections:

  • Resources: The AWS components to create.
  • Outputs: Stack outputs, such as URLs, ARN/IDs, etc.
  • Parameters: Input variables for the template. Strings like the WordPress database username and password would be parameters.

Creating the template

Using the knowledge above, we can now create a WordPress template. WordPress and our infrastructure require the following parameters:

  • AMI: The base image to use for our server. I’ve set a default below to an AWS Ubuntu image.
  • ApacheAdminEmailAddress: Apache asks for an admin email address.
  • EBSVolumeSize: The size of the hard drive on our web server.
  • InstanceType: The EC2 isntance type, such as t2.micro.
  • KeyName: A premade AWS key that you can use to SSH into the server.
  • MySQL Root Password: We’re installing MySQL and MySQL needs a root password.
  • MySQL WordPress Database Parameters: The DB name, username, and password for the instance.
  • Subnet and VPC: Where to put your instance. Find these in Networking –> VPC.
  • WordpressDomain: The URL for the new WordPress instance.

The WordPress template will use these parameters to create a security group (similar to a network firewall) and to create and configure the EC2 instance that will host Apache and MySQL. It also has a WaitConditionHandle to signal to AWS that it has finished.

Please note that a vast majority of this template is the initialization script under the EC2 instance’s UserData section.

First, here’s the template without the Parameters and UserData sections.

{
	"Description" : "Wordpress single instance",
	"Outputs": {
		"InstanceID": {
			"Description": "The wordpress instance id",
			"Value": { "Ref": "WordpressInstance" }
		}
	},
	"Parameters": {
		...
	},
	"Resources": {
		"WordpressSecurityGroup": {
			"Type": "AWS::EC2::SecurityGroup",
			"Properties": {
				"VpcId": {"Ref": "VPC"},
				"GroupDescription": "Allow SSH, HTTP, and HTTPS access",
				"SecurityGroupIngress": [
					{
						"IpProtocol": "tcp",
						"FromPort": "22",
						"ToPort": "22",
						"CidrIp": "0.0.0.0/0"
					},
					{
						"IpProtocol": "tcp",
						"FromPort": "80",
						"ToPort": "80",
						"CidrIp": "0.0.0.0/0"
					}
				]
			}
		},
		"WordpressInstance": {
			"Type" : "AWS::EC2::Instance",
			"Properties": {
				"BlockDeviceMappings" : [{
					"DeviceName" : "/dev/sda1",
					"Ebs" : {"VolumeSize" : {"Ref" : "EBSVolumeSize"}}
				}],
				"SecurityGroupIds": [{"Ref": "WordpressSecurityGroup"}],
				"KeyName": {"Ref": "KeyName" },
				"ImageId": {"Ref": "AMI"},
				"SubnetId": {"Ref": "Subnet"},
				"InstanceType": {"Ref": "InstanceType"},
				"Tags": [
					{"Key" : "Name", "Value" : "wordpress-instance-for-ami"}
				],
				"UserData": {"Fn::Base64": {"Fn::Join": ["\n", [
					...
                ]]}}
			}
		},
		"WaitHandle": {
			"Type": "AWS::CloudFormation::WaitConditionHandle"
		},
		"WaitCondition": {
			"Type": "AWS::CloudFormation::WaitCondition",
			"DependsOn": "WordpressInstance",
			"Properties": {
				"Count": "1",
				"Handle": {"Ref" : "WaitHandle"},
				"Timeout": "1800"
			}
		}
	}
}

The resources section has four components:

  • Security Group: A firewall that permits access to SSH and HTTP from anywhere. You can change this to be more restrictive if you’d prefer.
  • EC2 Instance: Our shared web and database server. The top section of the EC2 resource is pretty self-explanatory since it just uses the input parameters from the template
  • WaitHandle: Used by the instance to signal when it has finished loading.
  • WaitCondition: A condition that will fail the CloudFormation stack creation if the WaitHandle isn’t signaled before 1800 seconds elapses.

With the general structure clear, here is the full template in all its glory.

{
	"Description" : "Wordpress single instance",
	"Outputs": {
		"InstanceID": {
			"Description": "The wordpress instance id",
			"Value": { "Ref": "WordpressInstance" }
		}
	},
	"Parameters": {
		"AMI": {
			"Description": "The Amazon Ubuntu AMI",
			"Type": "String",
			"Default": "ami-69631053"
		},
		"ApacheAdminEmailAddress": {
			"Description": "Admin email address for Apache's ServerAdmin directive",
			"Type": "String",
			"Default": "webermaster@localhost"
		},
		"EBSVolumeSize": {
			"Description": "The size of the EBS volume for the transcoder",
			"Type": "String",
			"Default": "100"
		},
		"InstanceType": {
			"AllowedValues": [
				"t2.micro",
				"t2.small",
				"t2.medium",
				"t2.large",
				"c4.large",
				"c4.xlarge",
				"c4.2xlarge",
				"c4.4xlarge",
				"c4.8xlarge"
			],
			"ConstraintDescription": "must be a valid EC2 instance type",
			"Default": "t2.micro",
			"Description": "EC2 instance type",
			"Type": "String"
		},
		"KeyName": {
			"AllowedPattern": "[ -~]*",
			"ConstraintDescription": "can contain only ASCII characters",
			"Description": "Name of an existing EC2 KeyPair to enable SSH access",
			"MaxLength": "255",
			"MinLength": "1",
			"Type": "String"
		},
		"MySQLRootPassword": {
			"Description": "The MySQL root password",
			"Type": "String"
		},
		"MySQLWordpressDBName": {
			"Description": "The MySQL wordpress database name",
			"Type": "String",
			"Default": "wordpress"
		},
		"MySQLWordpressDBUsername": {
			"Description": "The MySQL wordpress database username",
			"Type": "String",
			"Default": "wordpress"
		},
		"MySQLWordpressDBPassword": {
			"Description": "The MySQL password",
			"Type": "String"
		},
		"Subnet": {
			"Description": "The subnet to contain the instance",
			"Type": "String"
		},
		"VPC": {
			"Description": "VPC",
			"Type": "String"
		},
		"WordpressDomain": {
			"Description": "The domain without www for the wordpress instance, i.e. example.com",
			"Type": "String"
		}
	},
	"Resources": {
		"WordpressSecurityGroup": {
			"Type": "AWS::EC2::SecurityGroup",
			"Properties": {
				"VpcId": {"Ref": "VPC"},
				"GroupDescription": "Allow SSH, HTTP, and HTTPS access",
				"SecurityGroupIngress": [
					{
						"IpProtocol": "tcp",
						"FromPort": "22",
						"ToPort": "22",
						"CidrIp": "0.0.0.0/0"
					},
					{
						"IpProtocol": "tcp",
						"FromPort": "80",
						"ToPort": "80",
						"CidrIp": "0.0.0.0/0"
					}
				]
			}
		},
		"WordpressInstance": {
			"Type" : "AWS::EC2::Instance",
			"Properties": {
				"BlockDeviceMappings" : [{
					"DeviceName" : "/dev/sda1",
					"Ebs" : {"VolumeSize" : {"Ref" : "EBSVolumeSize"}}
				}],
				"SecurityGroupIds": [{"Ref": "WordpressSecurityGroup"}],
				"KeyName": {"Ref": "KeyName" },
				"ImageId": {"Ref": "AMI"},
				"SubnetId": {"Ref": "Subnet"},
				"InstanceType": {"Ref": "InstanceType"},
				"Tags": [
					{"Key" : "Name", "Value" : "wordpress-instance-for-ami"}
				],
				"UserData": {"Fn::Base64": {"Fn::Join": ["\n", [
					"#!/bin/bash",

					"cat >/tmp/install.sh << 'INSTALL'",

						"/bin/bash -ex",
						"",

						"# APT",
						"apt-get -y update",
						{"Fn::Join": ["", [
							"debconf-set-selections <<< 'mysql-server mysql-server/root_password password ",
							{"Ref": "MySQLRootPassword"}, "'"
						]]},
						{"Fn::Join": ["", [
							"debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password ",
							{"Ref": "MySQLRootPassword"}, "'"
						]]},
						{"Fn::Join": ["", [
							"debconf-set-selections <<< 'postfix postfix/mailname string ",
							{"Ref": "WordpressDomain"}, "'"
						]]},
						"debconf-set-selections <<< \"postfix postfix/main_mailer_type string 'Internet Site'\"",
						"apt-get -y install python-setuptools awscli apache2 php5 mysql-server php5-mysql postfix unzip",

						"# Installation workspace",
						"if [ ! -d ~/install ]; then mkdir /tmp/install; fi",
						"cd /tmp/install",

						"# Amazon Tools",
						"curl -o aws-cfn-bootstrap-latest.tar.gz https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz",
						"tar -zxf aws-cfn-bootstrap-latest.tar.gz --strip-components 1",
						"easy_install .",

						"# Apache/WordPress",
						"wget https://wordpress.org/latest.zip",
						"unzip latest.zip",
						"mv wordpress /var/www/wordpress",
						"chown -R www-data.www-data /var/www/wordpress",
						"cat >/etc/apache2/sites-available/wordpress.conf << 'APACHE_SITE'",
							"<VirtualHost *:80>",
							{"Fn::Join": ["", ["    ServerName ", {"Ref": "WordpressDomain"}]]},
							{"Fn::Join": ["", ["    ServerAlias www.", {"Ref": "WordpressDomain"}]]},
							{"Fn::Join": ["", ["    ServerAdmin ", {"Ref": "ApacheAdminEmailAddress"}]]},
							"    DocumentRoot /var/www/wordpress",
							"    <Directory \"/var/www/wordpress\">",
							"        AllowOverride all ",
							"    </Directory>",
							"    ErrorLog ${APACHE_LOG_DIR}/error.log",
							"    CustomLog ${APACHE_LOG_DIR}/access.log combined",
							"</VirtualHost>",
						"APACHE_SITE",
						"ln -s /etc/apache2/sites-available/wordpress.conf /etc/apache2/sites-enabled/wordpress.conf",
						"sudo service apache2 restart",

						"# MySQL database",
						"cat >/tmp/setup.sql << 'MYSQL_COMMANDS'",
							{"Fn::Join": ["", [
								"CREATE DATABASE `",
								{"Ref": "MySQLWordpressDBName"}, "`;"
							]]},
							{"Fn::Join": ["", [
								"CREATE USER '",
								{"Ref": "MySQLWordpressDBUsername"},
								"'@'localhost' IDENTIFIED BY '",
								{"Ref": "MySQLWordpressDBPassword"},
								"';"
							]]},
							{"Fn::Join": ["", [
								"GRANT ALL ON `",
								{"Ref": "MySQLWordpressDBName"},
								"`.* to '",
								{"Ref": "MySQLWordpressDBUsername"},
								"'@'localhost';"
							]]},
						"MYSQL_COMMANDS",
						{"Fn::Join": ["", [
							"mysql -uroot -p",
							{"Ref": "MySQLRootPassword"},
							" < /tmp/setup.sql"
						]]},

						"# Postfix",
						"sudo sed -i '/inet_interfaces = all/c\\inet_interfaces = loopback-only' /etc/postfix/main.cf",

					"INSTALL",

					"# Run install.sh",
					"bash -e -x /tmp/install.sh",

					"# Cleanup",
					"shred -u -x -n 7 /tmp/install.sh",
					"shred -u -x -n 7 /tmp/setup.sql",
					"rm -rf /tmp/install",

					{"Fn::Join": ["", ["/usr/local/bin/cfn-signal --exit-code $? '", {"Ref" : "WaitHandle"}, "'"]]}
				]]}}
			}
		},
		"WaitHandle": {
			"Type": "AWS::CloudFormation::WaitConditionHandle"
		},
		"WaitCondition": {
			"Type": "AWS::CloudFormation::WaitCondition",
			"DependsOn": "WordpressInstance",
			"Properties": {
				"Count": "1",
				"Handle": {"Ref" : "WaitHandle"},
				"Timeout": "1800"
			}
		}
	}
}

The confusing part is likely the giant resources section. The bulk of the configuration lies in the UserData script of the EC2 instance resource. This will run when the box boots up for the first time. It installs all the necessary software and configures it using our input parameters. Some things to note:

  • Fn::Join: An AWS CloudFormation function that concatenates strings using a separator. It is often heavily used in complex CloudFormation scripts like the one above.
  • Ref: Another function used above to get the values of input parameters. It can also be used to get the default ARN or URL of a resource that is created within the template, such as {"Ref": "WordpressSecurityGroup"}.
  • Fn::Base64: Base64 encodes the UserData script as required.

The rest of the UserData script does what a Systems Administrator would normally do when making an all-in-one WordPress server: it installs OS software, configures the database and web server, and downloads and installs WordPress.

Feel free to upload this template into the CloudFormation section of the AWS console, enter your preferred values for the parameters above, and watch the magic happen.

Conclusion

In the real world, having a single web server and putting it on the same machine as a single database server is not a good idea, but it does the trick for explaning how CloudFormation works.

If you have any comments or questions, please post in the comments below. Thanks!