Tutorial: Create a Three-Tier WordPress Application in AWS with Terraform — Part Two

Dan Phillips
Version 1
Published in
9 min readNov 2, 2023

In the first part of our tutorial on building a WordPress application in AWS, we created our Virtual Private Cloud (VPC), into which we will place our WordPress application and a MySQL database in separate private subnets across two availability zones.

In this section, we’re going to begin by creating the security rules that govern how our resources communicate with the internet, and each other, before moving onto creating the relational database that WordPress needs, adding it to the private data subnet we created in our VPC.

To achieve this, we are going to use the managed AWS Relational Database Service (RDS).

Photo by Caspar Camille Rubin on Unsplash

We don’t have to use RDS for our WordPress project, but we do have to use a MySQL database engine, which RDS offers, among several compelling benefits.

First and foremost, it takes care of the undifferentiated heavy lifting of database management tasks such as provisioning, patching, backups, and high availability, allowing developers and administrators to focus on application development rather than database maintenance.

RDS also supports multiple popular database engines like MySQL (which we are using), PostgreSQL, Oracle, and SQL Server, providing flexibility and compatibility with a wide range of applications. Additionally, it offers automated backups with retention policies and automated software patching to enhance database security and reliability. Scaling database resources up or down to meet performance demands is also straightforward with RDS, and it supports read replicas for improved read scalability.

AWS also provides monitoring and alerting capabilities through CloudWatch, enhancing the observability of our database instances.

Overall, AWS RDS simplifies database management, improves scalability, and ensures high availability, making it an excellent choice for anyone looking to leverage the cloud for their database needs, and our project is an ideal opportunity to get our feet wet with it.

Find out more about AWS RDS here.

Application Layer

As it stands, you should have a parent directory to hold your application layers, with the following structure:

aws_wordpress_demo
network
# ...network configuration files

We’ll begin the next steps with your network applied and built-in AWS (as we ended part one of our guide). Open a terminal and CD into the root of your project folder (I’ve called mine aws_wordpress_demo), create an application directory and CD into it:

cd aws_wordpress_demo && mkdir application && cd application

As we’re creating a new layer, we need to repeat the first steps from our network layer and create a provider.tf file to let Terraform know which providers our configuration will use and configure the necessary settings for them, and a backend.tf file, to let Terraform know where to store our remote state file for this layer of our application:

touch provider.tf backend.tf
# provider.tf

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.13.1"
}
}
}

provider "aws" {
region = "eu-west-1"
}
# backend.tf

terraform {
backend "s3" {
bucket = "wordpress-tutorial-state-store"
key = "application/terraform.tfstate"
region = "eu-west-1"

}
}

As with our network layer, here we are telling Terraform to store our state file in the same S3 bucket we created in part one, but in an application directory.

With these files in place, we can initialise our application layer from the terminal, which will enable Terraform to be able tocreate our infrastructure as we develop it:

terraform init
Successfully Initialising our database layer in Terraform.

Accessing the Network state file

As we’re building our application with a decoupled microservice approach, we need to access the state file from our network layer to be able to identify our VPC and allocate resources to it:

touch data.tf
# data.tf

data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "wordpress-tutorial-state-store"
key = "network/terraform.tfstate"
region = "eu-west-1"
}
}

In Terraform, the “data” resource serves as a mechanism for fetching and utilizing external data or information within our infrastructure-as-code (IaC) configuration. It enables us to query and retrieve data from various sources, such as cloud providers, APIs, local files, or in our case, remote files in an S3 bucket, and incorporate that data into our Terraform configuration.

This retrieved data can be used for dynamic decision-making, configuration parameterization, or as input for resource creation, allowing us to create flexible and adaptable infrastructure configurations that respond to external information and changes in our environment.

Within our network layer In part one, we created outputs.tf file to output certain values, such as our VPC and subnet id’s. From the imported data above, we are now able to access those id’s from within our current application layer.

Security Groups

Next, in your terminal, create a security_groups.tf file and open it in your IDE:

touch security_groups.tf
# security_groups.tf

# load balancer security group
resource "aws_security_group" "ALB_SG" {
name = "Load-Balancer-SG"
tags = {
Name = "Load-Balancer-SG"
}
vpc_id = data.terraform_remote_state.network.outputs.vpc
}

resource "aws_security_group_rule" "alb_ingress" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.ALB_SG.id
}

resource "aws_security_group_rule" "alb_egress" {
type = "egress"
from_port = 80
to_port = 80
protocol = "tcp"
security_group_id = aws_security_group.ALB_SG.id
source_security_group_id = aws_security_group.EC2_SG.id
}

# EC2/WordPress App Security Group
resource "aws_security_group" "EC2_SG" {
name = "EC2-SG"
tags = {
Name = "EC2-SG"
}
vpc_id = data.terraform_remote_state.network.outputs.vpc
}

resource "aws_security_group_rule" "ec2_ingress" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
security_group_id = aws_security_group.EC2_SG.id
source_security_group_id = aws_security_group.ALB_SG.id
}

resource "aws_security_group_rule" "ec2_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.EC2_SG.id
}

# RDS/Database Security Group
resource "aws_security_group" "RDS_SG" {
name = "RDS_SG"
tags = {
Name = "RDS_SG"
}
vpc_id = data.terraform_remote_state.network.outputs.vpc
}

resource "aws_security_group_rule" "rds_ingress" {
type = "ingress"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_group_id = aws_security_group.RDS_SG.id
source_security_group_id = aws_security_group.EC2_SG.id
}

Here, we are creating three Security Groups, deploying them within our VPV, and declaring inbound and outbound (ingress and egress) rules for each.

To begin, we create a security group for the Application Load Balancer (ALB) which will direct traffic to the instances in our private application subnets across our Availability Zones (AZ). We call this security group ALB_SG. We then create two rules:

  • alb_ingress accepts ingress traffic on port 80 from any IP address (cidr_blocks = [“0.0.0.0/0”]) — our Internet Gateway (IGW)
  • alb_egress allows outbound traffic on port 80 to the security group we will attach to our EC2 instances, thereby ensuring that the outbound traffic from our ALB ONLY goes to our WordPress application.
  • Each of these rules is attached to the ALB_SG SG with the “security_group_id = aws_security_group.ALB_SG.id” instruction.

Next, we create a security group for the EC2 instances which will host our WordPress installations. We call this EC2_SG. We then create two rules for this:

  • ec2_ingress accepts ingress traffic on port 80 from our ALB only, by setting the “source_security_group_id” parameter to the id of our ALB_SG security group.
  • ec2_egress allows outbound traffic on all ports and across all protocols to enable our EC2 to communicate with our Nat Gateway for installing WordPress, and also to our MySQL database via port 3306.
  • Each of these rules is attached to the EC2_SG security group with the “security_group_id = aws_security_group.EC2_SG.id” instruction.

Finally, we create a security group for our RDS database called RDS_SG, and create only one rule for it, to allow ingress traffic on port 3306, only from the EC2 security group we just created. We attach this rule to the RDS_SG the same way we did with our EC2 and ALB security group rules.

That’s it for our security groups: our ALB accepts inbound traffic from the Internet Gateway (IGW), our EC2’s (WordPress applications) accept inbound traffic from our ALB only, and our RDS accepts inbound traffic from our EC2 (WordPress applications) only.

AWS RDS

Next, we get to create our AWS RDS resource.

touch rds.tf
# rds.tf

resource "aws_db_instance" "default" {
allocated_storage = 20
max_allocated_storage = 100
db_name = "wordpress_db"
engine = "mysql"
engine_version = "8.0.33"
instance_class = "db.t2.micro"
username = "admin"
password = aws_secretsmanager_secret_version.db_pass.secret_string
storage_type = "gp2"
parameter_group_name = "default.mysql8.0"
skip_final_snapshot = true
identifier = "wordpress_db"
db_subnet_group_name = data.terraform_remote_state.network.outputs.db_subnet_group_name
vpc_security_group_ids = [aws_security_group.RDS_SG.id]
}

Here we define the configuration for our AWS RDS instance, including its specifications, storage, database engine, security, and other settings needed to create a MySQL database instance named “wordpress_db”.

Let’s break down the key attributes and their significance:

allocated_storage and max_allocated_storage: These parameters specify the initial and maximum storage capacity, respectively, for the RDS instance. In this case, the instance starts with 20 GB of storage and can scale up to a maximum of 100 GB.

db_name: Specifies the name of the database that will be created on the RDS instance. Here, ours is set to "wordpress_db."

engine and engine_version: These parameters define the database engine and its version. This code creates an RDS instance running MySQL 8.0.33. WordPress requires a MySQL database engine.

instance_class: Specifies the RDS instance type, which determines the computing resources allocated to the database. Here, it's set to "db.t2.micro," indicating a small, general-purpose instance type — perfect for our demo purposes.

username and password: These are used to set the credentials for the database's administrative user. We are using a generic “admin” value for theusername, and we will generate a secure password using AWS Secrets Manager.

storage_type: Sets the storage type for the RDS instance. "gp2" indicates General Purpose (SSD) storage.

parameter_group_name: Specifies the name of the parameter group to be associated with the RDS instance. Parameter groups are used to configure database engine settings.

skip_final_snapshot: When set to true, this prevents the creation of a final database snapshot when the RDS instance is deleted or destroyed. For our purposes, we do not need a final snapshot, but in a real-life production environment, you almost certainly would.

identifier: Provides a user-defined name for the RDS instance, which is set to "wordpress_db" in our configuration.

db_subnet_group_name: References a data source (obtained from the remote state of our network layer in the data resource above) to determine the name of the DB subnet group used for the RDS instance. We created this subnet group in our network layer and added its value to our output,tf file in the network layer.

vpc_security_group_ids: Specifies the security group(s) associated with the RDS instance. This security group is referenced from an AWS security group resource which we created above in security_groups.tf.

Passwords and Sensitive Values

Unsurprisingly, we don’t want to expose sensitive values in our code, such as ourdatabase password, to the outside world. While there are many ways of securing our access credentials, for our tutorial, we are going to make use of AWS Secrets Manager.

Secrets Manager is a fully managed service from AWS which is designed to help us securely store, manage, and rotate sensitive information such as passwords, API keys, and database credentials. It enables users to centralize the management of these secrets, reducing the risk of unauthorized access and simplifying the process of secret retrieval for applications and services. In your terminal, create a secrets.tf file:

touch secrets.tf
# secrets.tf

resource "aws_secretsmanager_secret" "db_pass" {
name_prefix = "db_password"
}

resource "aws_secretsmanager_secret_version" "db_pass" {
secret_id = aws_secretsmanager_secret.db_pass.id
secret_string = data.aws_secretsmanager_random_password.db_pass.random_password
}

data "aws_secretsmanager_random_password" "db_pass" {
password_length = 30
exclude_punctuation = true
}

Let’s break down what’s happening here:

  • resource “aws_secretsmanager_secret” “db_pass”: This block creates an AWS Secrets Manager secret resource named “db_pass”. The name_prefix attribute sets the name prefix for the secret to "db_password". This means the actual secret name will include this prefix followed by a unique identifier.
  • resource “aws_secretsmanager_secret_version” “db_pass”: here we create a new version of the “db_pass” secret, while secret_id references the secret created in the previous step by using aws_secretsmanager_secret.db_pass.id, and secret_string sets the secret’s content to a randomly generated password which it retrieves from a data source, defined next.
  • data “aws_secretsmanager_random_password” “db_pass”: this defines a data source to generate a random password. We set the password length to 30 characters and exclude punctuation, but there are many options available in the documentation for your chosen password parameters.

That’s it for this stage! This was a bigger step than it looks and we’ve covered a lot of ground together:

  1. We have created a strict set of security rules to manage how the outside world can access our application, and how its own resources communicate with each other inside our VPC.
  2. By using AWS RDS for our MySQL database, we have given ourselves an excellent managed database service which can scale, provides failover support, and can provide read replicas to take read demand away from our main read/write database among many other features.
  3. We have introduced AWS Secrets Manager for managing sensitive values —our password in this case. However, we will revisit this file and repeat this process in the next stage when we need to create more sensitive values required by our WordPress configuration.

In the next section, we’ll create the launch configuration for our WordPress instances, and we’ll look at what is happening behind the scenes when WordPress is installed on our EC2 instances.

The code repository for our tutorial at this stage can be found in this branch on GitHub. Feel free to connect with me on LinkedIn and GitHub.

About the author

Dan Phillips is an Associate AWS DevOps Engineer here at Version 1.

--

--

Dan Phillips
Version 1

I'm a DevOps and software engineer based in Newcastle-Upon-Tyne, UK.