Desplegar Wordpress con ECS (Fargate), RDS y EFS
Esta práctica es igual a la que ya está documentada aquí y aquí. Así que no voy a entrar en detalles más específicos, centrándome solo en Terraform y su código.
VPC
Vamos a necesitar una VPC como ya sabemos. Está hecho con un módulo, donde tendremos tres zonas y redes privadas y públicas. Este es el código usado:
Módulo de VPC
# VPC con módulo oficial
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.21.0"
name = "Conv_VPC"
# Network
cidr = "10.0.0.0/16"
azs = ["eu-central-1a", "eu-central-1b", "eu-central-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
}
Security Groups
Necesitaremos varios Security Groups para asegurar las conexiones entre servicios y también para dar acceso general a la página de Wordpress.
Security Group de WordPress
Para el Wordpress, se permite HTTP para todas las conexiones, y de salida completa para todo internet.
WordPress SG
Security Group del bastión EC2
El grupo de seguridad del bastión permite conexión por SSH para mi IP personal (Que no os voy a enseñar para que no me doxeeis) y salida a todo internet.
EC2 SG
resource "aws_security_group" "ec2_sg" {
name = "ec2_sg"
description = "Security group to allow HTTP and SSH access"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.ip_whitelist
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "ec2_sg"
}
}
Security Group de RDS
Para el de RDS, vamos a permitir la entrada por el puerto 3306 para el EC2 y también para Wordpress. De salida a todo internet.
RDS SG
resource "aws_security_group" "rds_sg" {
name = "rds_sg"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.ec2_sg.id]
description = "Allow MySQL access from EC2"
}
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.wordpress_sg.id]
description = "Allow ECS WordPress access"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Security Group de EFS
Para el EFS, solo le vamos a dar una regla de entrada para Wordpress por el puerto 2049.
EFS SG
resource "aws_security_group" "wordpress_efs_sg" {
name = "wordpress-efs-sg"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 2049
to_port = 2049
protocol = "tcp"
security_groups = [aws_security_group.wordpress_sg.id] # ECS tasks
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Par de llaves
Vamos a necesitar un par de keys para poder conectar a EC2 si lo necesitamos. Primero creamos las claves.
Ahora le asignamos a la clave privada un nombre, para poder asignársela después a EC2.
resource "aws_key_pair" "ssh-key" {
key_name = "server-key"
public_key = tls_private_key.terrafrom_generated_private_key.public_key_openssh
}
Y por último la clave pública la vamos a tener en local, aunque no es la mejor práctica, ya que lo mejor sería llevarla a Secrets Manager. Al tenerla en local será más fácil para trabajar en esta práctica.
resource "local_file" "cloud_pem" {
filename = "${path.module}/ec2_private_key.pem"
content = tls_private_key.terrafrom_generated_private_key.private_key_openssh
file_permission = "0600"
}
¡Cuidado!
Importante, al tenerlo en local, si lo subes a un repositorio de GitHub, recuerda añadir a tu .gitignore para que no se suba automáticamente. O bórralo antes. Si subes la llave pública, podrían entrar a tu EC2, ¡cuidao!
EC2
Para el bastión EC2, vamos a usar prácticamente el mismo código que en la primera práctica, pero con unos cambios esenciales. Primero, el data para la ami.
data "aws_ami" "latest_amazon_linux_image" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-kernel-*-hvm-*-x86_64-gp2"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
Para empezar, las configuraciones básicas de la EC2. La instancia, el par de claves que hicimos antes, el grupo de seguridad, la subnet de la VPC. También clave pública, pero no creo que sea necesario para este proyecto.
resource "aws_instance" "Conv_EC2" {
ami = data.aws_ami.latest_amazon_linux_image.id
instance_type = "t2.micro"
key_name = aws_key_pair.ssh-key.key_name
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
subnet_id = module.vpc.public_subnets[0]
associate_public_ip_address = true
Ahora el user data, esencial para el funcionamiento del proyecto. Vamos a necesitar crear la base de datos que usará Wordpress, además del usuario. Para ello, instalamos mysql, hacemos un sleep para que le de tiempo a RDS a estar listo, y luego lo que he comentado de MYSQL.
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y mysql
# Esperar a que el clúster RDS esté disponible
sleep 320
# Crear usuario en la base de datos
mysql -h ${module.cluster.cluster_endpoint} -u admin -ppassword -e "CREATE DATABASE IF NOT EXISTS wordpress;"
mysql -h ${module.cluster.cluster_endpoint} -u admin -ppassword -e "CREATE USER IF NOT EXISTS 'admin'@'%' IDENTIFIED BY 'password';"
mysql -h ${module.cluster.cluster_endpoint} -u admin -ppassword -e "GRANT ALL PRIVILEGES ON wordpress.* TO 'admin'@'%' WITH GRANT OPTION;"
mysql -h ${module.cluster.cluster_endpoint} -u admin -ppassword -e "FLUSH PRIVILEGES;"
EOF
}
RDS
El cluster de RDS también será un módulo. La versión del engine, y la instancia lo he encontrado en documentaciones externas, pero es perfecto para esta en concreto. Además, la instancia está bien para ello, ya que es la más barata.
´´´bash module "cluster" { source = "terraform-aws-modules/rds-aurora/aws"
name = "test-aurora-mysql"
engine = "aurora-mysql"
engine_version = "8.0.mysql_aurora.3.09.0"
instance_class = "db.t4g.medium"
instances = {
one = {}
}
´´´
Lo siguiente es algo que me ha amargado la existencia durante dos días. Para RDS, le asignamos un usuario y contraseña. Si solo pones master_username y master_password, Terraform por defecto va a ignorar la contraseña que le pongas y creará un Secret en AWS para usarlo automáticamente, para ello, hay que definir explícitamente que NO queremos que se haga de esa manera. Es contradictorio, ya que manage_master_user_password nos dice que al poner una contraseña, no se puede usar secrets, cosa que no es cierta.
Por último, el resto de configuraciones básicas. El VPC, la subnet, y configuraciones varias que son a gusto y comodidad.
vpc_id = module.vpc.vpc_id
db_subnet_group_name = "aurora-subnet-group"
create_db_subnet_group = true
subnets = module.vpc.private_subnets
vpc_security_group_ids = [aws_security_group.rds_sg.id]
storage_encrypted = true
apply_immediately = true
skip_final_snapshot = true
enabled_cloudwatch_logs_exports = []
tags = {
Environment = "dev"
Terraform = "true"
}
}
Secrets Manager
Vamos a crear un secrets manager para poder tener las credenciales de Wordpress, además del nombre de la base de datos, etc. Además le vamos a crear una política para que ECS pueda acceder a Secrets Manager y pueda coger toda la información necesaria.
module "secrets_manager_wordpress" {
source = "terraform-aws-modules/secrets-manager/aws"
version = "1.3.1"
name = "wordpress-credentials-v5"
description = "Credenciales para WordPress ECS"
recovery_window_in_days = 0
# Permite acceso al secreto desde ECS
create_policy = true
block_public_policy = true
policy_statements = {
ecs_read_access = {
sid = "AllowEcsExecutionRoleToReadSecrets"
principals = [{
type = "AWS"
identifiers = ["arn:aws:iam::414131675413:role/ecsTaskExecutionRole"]
}]
actions = ["secretsmanager:GetSecretValue"]
resources = ["*"]
}
}
secret_string = jsonencode({
WORDPRESS_DB_HOST = module.cluster.cluster_endpoint
WORDPRESS_DB_NAME = "wordpress"
WORDPRESS_DB_USER = "admin"
WORDPRESS_DB_PASSWORD = "password"
})
tags = {
Environment = "dev"
Terraform = "true"
}
}
EFS
Necesitaremos el EFS para poder tener permanencia de archivos en WordPress.
resource "aws_efs_file_system" "wordpress" {
creation_token = "wordpress-efs"
encrypted = true
tags = {
Name = "wordpress-efs"
}
}
Vamos a crear un "grupo de variables", un locals. Para poder darle directamente a EFS las zonas disponibles en las que trabajará.
locals {
private_subnet_map = {
"az1" = module.vpc.private_subnets[0]
"az2" = module.vpc.private_subnets[1]
"az3" = module.vpc.private_subnets[2]
}
}
Y creamos el punto de montaje de EFS, que usará el locals que hemos creado ahora mismo, el EFS como tal, y el grupo de seguridad que le hicimos antes.
resource "aws_efs_mount_target" "wordpress" {
for_each = local.private_subnet_map
file_system_id = aws_efs_file_system.wordpress.id
subnet_id = each.value
security_groups = [aws_security_group.wordpress_efs_sg.id]
}
ECS
Ahora el cluster de ECS, le ponemos el nombre deseado. También le tenemos activado Container Insights, una herramienta de monitoreo.
resource "aws_ecs_cluster" "cluster" {
name = "ecs_terraform_convenio"
setting {
name = "containerInsights"
value = "enabled"
}
}
Task Definition
Las configuraciones básicas, servicio con su rol de ejecución, el tipo de network y usando Fargate.
resource "aws_ecs_task_definition" "task" {
family = "service"
execution_role_arn = "arn:aws:iam::414131675413:role/ecsTaskExecutionRole"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 512
memory = 1024
La definición del contenedor es directamente el JSON que usamos en esta parte. Lo hacemos así:
container_definitions = jsonencode([
{
name = "wordpress"
image = "wordpress:6.8.0-apache"
cpu = 0
essential = true
portMappings = [
{
containerPort = 80
hostPort = 80
protocol = "tcp"
name = "wordpress-80-tcp"
appProtocol = "http"
}
]
mountPoints = [
{
sourceVolume = "Wordpress-convenio"
containerPath = "/var/www/html"
readOnly = false
}
]
secrets = [
{
name = "WORDPRESS_DB_HOST"
valueFrom = "${module.secrets_manager_wordpress.secret_arn}:WORDPRESS_DB_HOST::"
},
{
name = "WORDPRESS_DB_NAME"
valueFrom = "${module.secrets_manager_wordpress.secret_arn}:WORDPRESS_DB_NAME::"
},
{
name = "WORDPRESS_DB_USER"
valueFrom = "${module.secrets_manager_wordpress.secret_arn}:WORDPRESS_DB_USER::"
},
{
name = "WORDPRESS_DB_PASSWORD"
valueFrom = "${module.secrets_manager_wordpress.secret_arn}:WORDPRESS_DB_PASSWORD::"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = "/ecs/wordpress-convenio"
awslogs-region = "eu-central-1"
awslogs-stream-prefix = "ecs"
}
}
}
])
Por último el volumen que usará el ECS, básicamente le decimos que use EFS.
volume {
name = "Wordpress-convenio"
efs_volume_configuration {
file_system_id = aws_efs_file_system.wordpress.id
root_directory = "/"
transit_encryption = "ENABLED"
authorization_config {
access_point_id = null
iam = "DISABLED"
}
}
}
}
Service
Para acabar, tanto con el ECS como con toda la configuración, el service. Será un servicio sencillo, le decimos que use siempre la última versión de la task, que solo tenga una, que use FARGATe, y todas las configuraciones básicas de red, cluster, etc.
resource "aws_ecs_service" "service" {
name = "service_conv"
cluster = aws_ecs_cluster.cluster.id
task_definition = aws_ecs_task_definition.task.arn
desired_count = 1
launch_type = "FARGATE"
platform_version = "LATEST"
network_configuration {
assign_public_ip = true
security_groups = [aws_security_group.wordpress_sg.id]
subnets = module.vpc.public_subnets
}
lifecycle {
ignore_changes = [task_definition]
}
tags = {
serviceName = "service_conv"
}
}
Repositorio
Podéis encontrar el proyecto completo y listo para usar en mi GitHub