This beginner’s guide explains how to use AWS CloudFormation to spin up an all-in-one WordPress server. This is an absolutely crazy thing to do in production; running every component on a single box with no redundancy or load balancing is a recipe for disaster. The purpose of this guide is to introduce CloudFormation using WordPress as an example — nothing 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’s suite of products is the AWS Console, a web-based UI that gives users 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 life with AWS. Better to focus on 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:
- 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 — the core topic of this article.
Fees
Every AWS product has 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’re doing. If eligible, try to stay within the free tier of each service; otherwise, use the smallest resource possible. For example, 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
This guide doesn’t cover creating an EC2 instance from the AWS console, but I’d recommend doing it now. It will help you understand what CloudFormation does below. The process should be pretty self-explanatory following this official Amazon guide, but here are a few extra tips:
- Use the
t2.microsize for test instances — they’re the cheapest. - You’ll need to choose an AMI, which will be the base operating system image for your new instance. Most people use the standard 64-bit Ubuntu AMI.
- A security group is essentially a firewall. You’ll want to permit SSH, HTTP, and HTTPS access to your machine.
- Save the
.pem(RSA) key when AWS provides it. This is how you’ll log in to your instance, and without it you’ll have to start over (which is more of a nuisance than a major issue at this point).
Using the AWS Command Line Tool to Make an EC2 Instance
It’s often easier to use Amazon’s command-line tool for tasks like uploading files to S3, deploying CloudFormation templates, and many others. That’s not the case for spinning up an EC2 instance, but it’s nice to know it can be done that way.
Worth noting: our Amazon representative at work mentioned that the command-line tool is generally more up-to-date than the AWS Console, so it’s a good addition to your AWS management arsenal.
The command-line tool is a Python program. To install it, follow these guides:
This pseudocode will create an EC2 instance, assuming you know your default VPC ID (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. It also lets you group components into stacks and configure them from 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 showing 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 and ARNs/IDs.
- 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 for our server. I’ve set the default below to an AWS Ubuntu image.
- ApacheAdminEmailAddress: Apache requires an admin email address.
- EBSVolumeSize: the size of the hard drive on our web server.
- InstanceType: the EC2 instance type, such as
t2.micro. - KeyName: a pre-made AWS key that you can use to SSH into the server.
- MySQLRootPassword: we’re installing MySQL, which needs a root password.
- MySQL WordPress database parameters: the database name, username, and password for the instance.
- Subnet and VPC: where to put your instance. Find these under Networking → VPC.
- WordpressDomain: the URL for the new WordPress instance.
The WordPress template uses these parameters to create a security group
(similar to a network firewall) and to create and configure the EC2 instance
that hosts Apache and MySQL. It also includes a WaitConditionHandle to
signal AWS when it has finished.
Note that the vast majority of this template is the initialization script in
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 SSH and HTTP access from anywhere. You can make this more restrictive if you’d prefer.
- EC2 Instance: our shared web and database server. The top section of the EC2 resource is fairly self-explanatory since it just uses the template’s input parameters.
- WaitHandle: used by the instance to signal when it has finished loading.
- WaitCondition: a condition that fails the CloudFormation stack creation if the WaitHandle isn’t signaled within 1800 seconds.
With the general structure clear, here’s 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 lives in the UserData script of the EC2 instance resource,
which runs when the box first boots up. It installs all the necessary software
and configures it using our input parameters. A few things to note:
- Fn::Join: an AWS CloudFormation function that concatenates strings using a separator. It’s heavily used in complex CloudFormation scripts like the one above.
- Ref: another function used above to retrieve the values of input
parameters. It can also retrieve the default ARN or URL of a resource created
within the template, such as
{"Ref": "WordpressSecurityGroup"}. - Fn::Base64: Base64-encodes the
UserDatascript as required.
The rest of the UserData script does what a systems administrator would
normally do when setting up an all-in-one WordPress server: it installs OS
packages, 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, putting a single web server on the same machine as a single database server is not a good idea, but it does the trick for explaining how CloudFormation works.
If you have any comments or questions, please post them below. Thanks!