Create a WordPress stack with AWS ECS and RDS database using Terraform
This article is a continuation of two previous articles. In the first one, we created an Ansible role including tests with the Molecule framework. From this base, we saw how to build a Docker image with Packer and Ansible in the second article. Now we have the WordPress image present in the AWS Elastic Container Registry (ECR).
Here we are going to write the Terraform code to deploy WordPress with Elastic Container Service (ECS). ECS is a fully managed container orchestration service. We will use AWS Fargate for containers for serverless compute engine. Β In addition to ECS, we will use Relational Database Service (RDS) for the MySQL database.
The presented Terraform code here uses an AWS S3 bucket to store remote states. Each component like RDS is a module included in a dedicated layer. The dependencies between modules are solved through the remote states in S3. Therefore, the presentation order on this page matters. You can use the AWS Free Tier Program to deploy code for free. The presented architecture is not fully high-available to avoid costs and to be reproducible. The full code project is here.
Deploy AWS VPC & Subnets
For the Virtual Private Network (VPC) resources, we are going to use the default one to avoid extra cost. We use a single availability zone and one NAT gateway to avoid network transfer costs.
Create the aws-vpc
layer in terraform/environments/dev/main.tf
:
Create the aws-vpc
module in terraform/modules/aws-vpc
:
In terraform/environments/dev/aws-vpc
initialize and apply the layer:
$ terraform init
$ terraform apply
Deploy The MySQL Database With AWS RDS
For the database, we are going to configure and deploy a single RDS (Relational Database Service) instance running MySQL.
Create the aws-rds
layer in terraform/environments/dev/aws-rds
:
Create the aws-rds
module in terraform/modules/aws-rds
:
The password is generated using Terraform resources to not expose sensitive information in the code.
In terraform/environments/dev/aws-rds
initialize and apply the layer:
$ terraform init
$ terraform apply
Deploy The AWS ECS Cluster
Create the aws-ecs
layer in terraform/environments/dev/aws-ecs
:
terraform {
required_version = "> 0.15.0"
backend "s3" {
bucket = "YOUR_BUCKET"
key = "dev/ecs-ansible-packer-terraform-wordpress/aws-ecs.tf"
encrypt = true
region = "us-east-1"
}
}
module "aws_ecs" {
source = "../../../modules/aws-ecs"
region = "us-east-1"
tags = {
environment = "dev"
project = "ecs-wordpress"
terraform = true
}
}
output "ecs_cluster_id" {
value = module.aws_ecs.cluster_id
}
output "ecs_cloudwatch_group_name" {
value = module.aws_ecs.cloudwatch_group_name
}
Create the aws-ecs
module in terraform/modules/aws-ecs
:
locals {
name = "${var.tags["environment"]}-${var.tags["project"]}-ecs"
}
For observability, we create an AWS Cloudwatch group to watch the container's logs:
resource "aws_ecs_cluster" "default" {
name = local.name
tags = merge({
name = local.name
}, var.tags)
}
resource "aws_cloudwatch_log_group" "default" {
name = local.name
tags = merge({
name = local.name
}, var.tags)
}
view raw
output "cluster_id" {
description = "The ECS cluster ID"
value = aws_ecs_cluster.default.id
}
output "cloudwatch_group_name" {
description = "The Cloudwatch group name to store container logs"
value = aws_cloudwatch_log_group.default.name
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.45.0"
}
}
}
provider "aws" {
region = var.region
}
variable "tags" {
type = map(string)
description = "(Required) Tags for resources"
}
variable "region" {
type = string
description = "(Required) AWS region"
}
In terraform/environments/dev/aws-ecs
initialize and apply the layer :
$ terraform init
$ terraform apply
Deploy WordPress Application In AWS ECS
Create the aws-ecs-wordpress
layer in terraform/environments/dev/aws-ecs-wordpress
:
terraform {
required_version = "> 0.15.0"
backend "s3" {
bucket = "YOUR_BUCKET"
key = "dev/aws-ecs-wordpress/aws-ecs-wordpress.tf"
encrypt = true
region = "us-east-1"
}
}
data "terraform_remote_state" "aws_ecs" {
backend = "s3"
config = {
bucket = "YOUR_BUCKET"
key = "dev/ecs-ansible-packer-terraform-wordpress/aws-ecs.tf"
region = "us-east-1"
}
}
data "terraform_remote_state" "aws_ecr" {
backend = "s3"
config = {
bucket = "YOUR_BUCKET"
key = "dev/ecs-ansible-packer-terraform-wordpress/aws-ecr.tf"
region = "us-east-1"
}
}
data "terraform_remote_state" "aws_rds" {
backend = "s3"
config = {
bucket = "YOUR_BUCKET"
key = "dev/ecs-ansible-packer-terraform-wordpress/aws-rds.tf"
region = "us-east-1"
}
}
data "terraform_remote_state" "aws_vpc" {
backend = "s3"
config = {
bucket = "YOUR_BUCKET"
key = "dev/ecs-ansible-packer-terraform-wordpress/aws-vpc.tf"
region = "us-east-1"
}
}
locals {
# Networking
vpc_id = data.terraform_remote_state.aws_vpc.outputs.vpc_id
subnet_id = data.terraform_remote_state.aws_vpc.outputs.subnet_id
sg_id = data.terraform_remote_state.aws_vpc.outputs.sg_id
# RDS
db_username = data.terraform_remote_state.aws_rds.outputs.db_username
db_host = data.terraform_remote_state.aws_rds.outputs.db_address
db_password = data.terraform_remote_state.aws_rds.outputs.db_password
db_port = data.terraform_remote_state.aws_rds.outputs.db_port
# ECS
ecs_cloudwatch_log_group_name = data.terraform_remote_state.aws_ecs.outputs.ecs_cloudwatch_group_name
ecs_cluster_id = data.terraform_remote_state.aws_ecs.outputs.ecs_cluster_id
# ECR
repository_url = data.terraform_remote_state.aws_ecr.outputs.repository_url
}
module "aws-ecs-wordpress" {
source = "../../../modules/aws-ecs-wordpress"
region = "us-east-1"
vpc_id = local.vpc_id
subnet_id = local.subnet_id
sg_id = local.sg_id
cloudwatch_log_group_name = local.ecs_cloudwatch_log_group_name
wordpress_db_host = local.db_host
wordpress_db_name = "wordpress"
wordpress_db_user = local.db_username
wordpress_db_password = local.db_password
wordpress_db_port = local.db_port
wordpress_port = 80
repository_url = local.repository_url
image_tag = "latest"
desired_count = 1
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html
fargate_cpu = 256
fargate_memory = 512
ecs_cluster_id = local.ecs_cluster_id
tags = {
environment = "dev"
project = "ecs-wordpress"
terraform = true
}
}
output "wordpress_admin_password" {
description = "The Wordpress admin password"
value = module.aws-ecs-wordpress.wordpress_admin_password
sensitive = true
}
To deploy WordPress containers in the ECS cluster, we need to define :
- An ECS service that allows to run and maintain a specified number of instances of a task definition. It will monitor the tasks if instances fail or stop and schedule new instances to meet the desired number of instances.
- An ECS task definition defining how to run the Docker containers in AWS ECS
In addition to that, we will create a dedicated security group and IAM permissions for the ECS service.
Create the aws-ecs-wordpress
module in terraform/modules/aws-ecs-wordpress
:
locals {
name = "${var.tags["environment"]}-${var.tags["project"]}"
}
resource "aws_iam_role" "ecs_task_execution_role" {
name = "ecs-execution-role"
assume_role_policy = <<EOF
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_iam_policy" "ecs_task_execution_policy" {
name = "ecs-execution-policy"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
EOF
}
# Attach execution policy to execution role
resource "aws_iam_role_policy_attachment" "ecs_role_attach" {
role = aws_iam_role.ecs_task_execution_role.name
policy_arn = aws_iam_policy.ecs_task_execution_policy.arn
}
resource "aws_security_group" "ecs_tasks" {
name = "wordpress-ecs-sg"
description = "Allow inbound access in port 80 only"
vpc_id = var.vpc_id
ingress {
protocol = "tcp"
from_port = var.wordpress_port
to_port = var.wordpress_port
cidr_blocks = [
"0.0.0.0/0"]
}
egress {
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = [
"0.0.0.0/0"]
}
tags = merge({
name = local.name
}, var.tags)
}
resource "random_string" "wordpress_admin_password" {
length = 16
}
resource "aws_ecs_task_definition" "task" {
family = "wordpress"
network_mode = "awsvpc"
requires_compatibilities = [
"FARGATE"]
cpu = var.fargate_cpu
memory = var.fargate_memory
task_role_arn = aws_iam_role.ecs_task_execution_role.arn
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
container_definitions = jsonencode([
{
name = local.name
essential = true
image = "${var.repository_url}:${var.image_tag}"
environment = [
{
name = "WP_DB_WAIT_TIME"
value = "1"
},
{
name = "WP_VERSION"
value = var.wordpress_version
},
{
name = "TZ"
value = var.wordpress_timezone
},
{
name = "WP_DB_HOST"
value = var.wordpress_db_host
},
{
name = "WP_DB_NAME"
value = var.wordpress_db_name
},
{
name = "WP_DB_USER"
value = var.wordpress_db_user
},
{
name = "MYSQL_ENV_MYSQL_PASSWORD"
value = var.wordpress_db_password
},
{
name = "WP_DOMAIN"
value = var.wordpress_domain
},
{
name = "WP_URL"
value = var.wordpress_url
},
{
name = "WP_LOCALE",
value = var.wordpress_locale
},
{
name = "WP_SITE_TITLE"
value = var.wordpress_site_title
},
{
name = "WP_ADMIN_USER"
value = var.wordpress_admin_user
},
{
name = "WP_ADMIN_PASSWORD"
value = random_string.wordpress_admin_password.result
},
{
name = "WP_ADMIN_EMAIL"
value = var.wordpress_admin_email
}
],
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = var.cloudwatch_log_group_name
awslogs-region = var.region
awslogs-stream-prefix = "ecs"
}
},
portMappings = [
{
hostPort = var.wordpress_port
containerPort = var.wordpress_port
protocol = "TCP"
}
]
}
])
tags = merge({
name = local.name
}, var.tags)
}
resource "aws_ecs_service" "default" {
name = local.name
cluster = var.ecs_cluster_id
task_definition = aws_ecs_task_definition.task.arn
desired_count = var.desired_count
launch_type = "FARGATE"
network_configuration {
security_groups = [
var.sg_id,
aws_security_group.ecs_tasks.id]
subnets = [
var.subnet_id]
assign_public_ip = true
}
tags = merge({
name = local.name
}, var.tags)
}
output "wordpress_admin_password" {
description = "The Wordpress admin password"
value = random_string.wordpress_admin_password.result
sensitive = true
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.46.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.1.0"
}
}
}
provider "aws" {
region = var.region
}
In terraform/environments/dev/aws-ecs-wordpress
initialize and apply the layer:
$ terraform init
$ terraform apply
In the AWS web console, you can see the ECS cluster with the Fargate service including the WordPress running task :
When you click on the ECS service you have more information about the task. Click on the task ID :
The container logs are searchable in the created log group in Cloudwatch:
When you click on the log group you can watch the container's logs:
Conclusion
Through this series of articles, we have seen how to set up a framework to configure, build and deploy images in ECS :
- Ansible manages the configuration recipes. The Ansible roles are reusable and testable using Molecule and Docker. It ensures good quality and improves development velocity.
- Packer reuses the Ansible code to build a docker image locally or push it to the AWS Elastic Registry.
- Terraform deals with the AWS infrastructure.