Part 1 - Architecture, Database and Infrastructure.
The following 3-part series of documents will go through setting up a containerized wordpress site, with an NGINX frontend, RDS/MariaDB database backend, build using Terraform and Ansible; and running AWS.
I wanted to make the simplest setup to run the docker wordpress site with an RDS/MariaDB backend. So, no ELB (load balancer), no Route53 or CloudFront.
Here's a simple diagram of what I want to build
Infrastructure Overview
This super basic setup consists of:
- 1 x EC2 instance with docker, docker-compose installed
- 1 x RDS MariaDB Database.
That's it.
I want Terraform to take care of standing all the infrastructure up.
Your file list (see Github)
instance.tf
outputs.tf
provider.tf
rds.tf
securitygroups.tf
variables.tf
Let's take a quick look at each component
EC2 Instance
What we're working with:
- OS is Ubuntu (AMI)
- instance type/size is 't2.micro'
- root device is 8GB and will be deleted when instance is terminated
- we want the instance to be assigned a Public IP address
- an 20GB EBS volume will be added, and available in the "us-east-1c" AZ
- EBS volume will be attached under device name "/dev/xvdb"
resource "aws_instance" "web01" {
# ubuntu ami
ami = "ami-2d39803a"
instance_type = "t2.micro"
vpc_security_group_ids = ["${aws_security_group.web_server_sg.id}"]
root_block_device {
volume_size = "8"
delete_on_termination = true
}
associate_public_ip_address = true
ebs_optimized = false
key_name = "infra_builder"
tags {
Name = "web01"
}
}
resource "aws_ebs_volume" "web01-ebs-volume1" {
availability_zone = "us-east-1c"
size = "20"
type = "gp2"
tags {
Name = "Data Volume"
CreatedBy = "Terraform"
}
}
resource "aws_volume_attachment" "web01-ebs1" {
device_name = "/dev/xvdb"
volume_id = "${aws_ebs_volume.web01-ebs-volume1.id}"
instance_id = "${aws_instance.web01.id}"
}
the key_name
you see here must be setup manually via the AWS console.
RDS Database
What's happening here:
- a mariadb with 5 GB storage
- its a
db.t2.medium
instance class (anything smaller becomes a problem) - set the username, password, license model (important as the wrong license throws errors)
skip_final_snapshot
must be set to 'true'.- databases need subnet groups to straddle, one subnet same as the ec2 instance, and another one for other admin, replication, backup use.
################ DB INSTANCE ###############
resource "aws_db_instance" "backend_db" {
name = "rds_mysql"
allocated_storage = 5
engine = "mariadb"
storage_type = "gp2"
instance_class = "db.t2.medium"
identifier = "wordpressdb"
username = "wpress"
password = "wpress_247x"
parameter_group_name = "${aws_db_parameter_group.rds_param_group.id}"
license_model = "general-public-license"
# set to 'true' so we can destroy
skip_final_snapshot = true
# network config
db_subnet_group_name = "${aws_db_subnet_group.rds_subnet_group.id}"
vpc_security_group_ids = ["${aws_security_group.db_server_sg.id}"]
tags {
Name = "Backend Database"
CreatedBy = "Terraform"
}
}
################ DB SUBNET ###############
resource "aws_db_subnet_group" "rds_subnet_group" {
name = "rds subnet group"
# us-east-1c, us-east-1d
subnet_ids = ["subnet-39d6a014","subnet-7a5c7b33"]
tags {
Name = "RDS Subnet Group"
CreatedBy = "Terraform"
}
}
################ DB PARAMS ###############
resource "aws_db_parameter_group" "rds_param_group" {
name = "wp-db-mariadb10-1"
family = "mariadb10.1"
}
Security Groups
This is probably the glue that allows everything (once they're stood up) to access each other correctly. Security Grounds inbound and outbound rules are all defined and associated here.
The following will define TWO security groups. One for the web server instance(s), and the second one for the RDS backend database.
There's probably a better way to throw all these together as its just a simple/small configuration, but I like to still use practices that can scale if we decided to jump to a bigger setup. I'm sure this isn't quite what they mean by making all code "modular" with Infrastructure as Code (IaC), but it works on the same principle.
################ GROUPS ###############
# this is your web server GROUP config
resource "aws_security_group" "web_server_sg" {
name = "web_server_security"
description = "ALLOW inbound traffic to web server"
vpc_id = "vpc-534cfe35"
tags {
Name = "web_sg"
CreatedBy = "Terraform"
}
}
# this is your RDS database GROUP config
resource "aws_security_group" "db_server_sg" {
name = "rds_server_security"
description = "ALLOW inbound traffic to RDS/MySQL"
vpc_id = "vpc-534cfe35"
tags {
Name = "db_sg"
CreatedBy = "Terraform"
}
}
################ WEB RULES ###############
# the following are security group RULES you add
# to the security GROUP
resource "aws_security_group_rule" "ssh" {
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
# add to the WEB GROUP above
security_group_id = "${aws_security_group.web_server_sg.id}"
}
resource "aws_security_group_rule" "web_http" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
# add to the WEB GROUP above
security_group_id = "${aws_security_group.web_server_sg.id}"
}
resource "aws_security_group_rule" "web_https" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
# add to the WEB GROUP above
security_group_id = "${aws_security_group.web_server_sg.id}"
}
resource "aws_security_group_rule" "anywhere_outbound" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
# add to the WEB GROUP above
security_group_id = "${aws_security_group.web_server_sg.id}"
}
################ DB RULES ###############
resource "aws_security_group_rule" "db_rds_sg-1" {
type = "ingress"
from_port = "3306"
to_port = "3306"
protocol = "tcp"
# allow inbound from the WEB GROUP
source_security_group_id = "${aws_security_group.web_server_sg.id}"
# references the DB GROUP above
security_group_id = "${aws_security_group.db_server_sg.id}"
}
resource "aws_security_group_rule" "db_rds_sg-2" {
type = "egress"
from_port = "0"
to_port = "0"
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
# references the DB GROUP above
security_group_id = "${aws_security_group.db_server_sg.id}"
}
And the last 2 terraform files in the mix are:
Provider
Standard setup:
provider "aws" {
access_key = "${var.access_key}"
secret_key = "${var.secret_key}"
region = "${var.aws_region}"
}
Variables
then these variables get fed into provider (amongst others):
variable "access_key" {
default = "**************************"
}
variable "secret_key" {
default = "**************************"
}
variable "aws_region" {
description = "AWS region"
default = "us-east-1"
}
variable "aws_vpc_cidr" {
default = {
"us-east-1" = "172.31.0.0/16"
}
}
Outputs
these can be whatever you need outputted to stdout (i.e. your screen) and also tracked by terraform (via terraform output
) for future use.
- first output will look for ALL public_ip's it can find under aws_instance.web01 (needs to be a list i.e. '[]' wrapped)
- second output just the name of the mariadb that's created
output "ec2_public_ips" {
value = ["${aws_instance.web01.*.public_ip}"]
}
output "rds_mysql_db_name" {
value = "${aws_db_instance.backend_db.address}"
}
Plan it, Run it
A rather long video of running terraform plan
, and when all looks good, no error messages I run terraform apply
. At a certain point in the video, terraform errors out and I have to fix it on the fly (did it quicker than normal due to having seen this error before).
Don't let the thumbnail fool you, I switch screens while the RDS database is being created and show the AWS console where things are magically showing up:
After all's up & running, login to the new EC2 instance and check it can connect to the new RDS/Maridadb database! (grab the database name from the terraform output):
A few Quirky Points
A few things I ran into while working through this which were good to note:
- mariadb complained with I tried to use db.t2.small. Setting it to db.t2.medium fixed the issue.
- db password must be longer than 8 char (no kidding)
vpc_security_group_ids
in the terraform variables needs to be inside '[]' i.e.[${aws_security_groups.x.id}]
or it won't recognize your reference as a 'list'
# set to 'true' so we can destroy
skip_final_snapshot = true
or you get this error on terraform destroy
Error: Error applying plan:
1 error(s) occurred:
* aws_db_instance.backend_db (destroy): 1 error(s) occurred:
* aws_db_instance.backend_db: DB Instance FinalSnapshotIdentifier is required when a final snapshot is required
- dont forget a
ALL 0.0.0.0/0
egress rule on your WEB security group or the outbound request (I tried to telnet to the RDS and it didnt return anything)
Right, that's the end of Part I!
In Part II we will provision some things onto the new EC2 instance to get it ready for running docker and using docker-compose!