Ambiente wordpress com CDN provisionado como código — Terraform

paulofponciano

Paulo Ponciano

Posted on January 29, 2024

Ambiente wordpress com CDN provisionado como código — Terraform

ECS Fargate, EFS, Aurora Serverless V2, CloudFront e S3

Arquitetura


Código terraform aqui.

Pré-requisitos

  • Domínio público configurado no Route 53

  • $

Variáveis

# Environment

variable "tags" {
  description = "AWS Tags to add to all resources created."
  type        = map(string)
  default = {
    Terraform = "true"
    Owner     = "SRE Team"
    Env       = "dev"
  }
}

variable "aws_region" {
  description = "AWS Region (e.g. us-east-1, us-west-2, sa-east-1, us-east-2)"
  default     = "us-west-2"
}

variable "azs" {
  description = "AWS Availability Zones (e.g. us-east-1a, us-west-2b, sa-east-1c, us-east-2a"
  default = [
    "us-west-2a",
    "us-west-2b"
  ]
}

variable "env_prefix" {
  description = "Environment prefix for all resources to be created. e.g. customer name"
  default     = "wordp"
}

variable "environment" {
  description = "Name of the application environment. e.g. dev, prod, staging."
  default     = "dev"
}

variable "site_domain" {
  description = "The primary domain name of the website."
  default     = "wordpress.domain.pro"
}

variable "public_alb_domain" {
  description = "The public domain name of the ALB."
  default     = "alb.domain.pro"
}

variable "cf_price_class" {
  description = "The price class for this distribution. One of PriceClass_All, PriceClass_200, PriceClass_100."
  default     = "PriceClass_100"
}

variable "error_ttl" {
  description = "The minimum amount of time (in secs) that cloudfront caches an HTTP error code."
  default     = "30"
}

variable "desired_count" {
  description = "The number of instances of fargate tasks to keep running."
  default     = "1"
}
variable "log_retention_in_days" {
  description = "The number of days to retain cloudwatch logs."
  default     = "1"
}

# VPC parameters

variable "vpc_cidr" {
  description = "The VPC CIDR block."
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  description = "List of CIDR blocks for public subnets."
  default     = ["10.0.32.0/20", "10.0.48.0/20"]
}

variable "private_subnet_cidrs" {
  description = "List of CIDR blocks for private subnets."
  default     = ["10.0.64.0/20", "10.0.80.0/20"]
}

# Database parameters

variable "db_backup_retention_days" {
  description = "Number of days to retain database backups."
  default     = "1"
}

variable "db_backup_window" {
  description = "Daily time range during which automated backups for rds are created if automated backups are enabled using the BackupRetentionPeriod parameter. Time in UTC."
  default     = "05:00-07:00"
}

variable "db_max_capacity" {
  description = "The maximum Aurora capacity unit."
  default     = "2.0"
}

variable "db_min_capacity" {
  description = "The minimum Aurora capacity unit."
  default     = "1.0"
}

variable "db_name" {
  description = "Database name."
  default     = "wordp"
}

variable "db_master_username" {
  description = "Master username of db."
  default     = "wordp"
}

variable "db_master_password" {
  description = "Master password of db."
}

variable "db_engine_version" {
  description = "The database engine version."
  default     = "8.0.mysql_aurora.3.02.0"
}

# Task parameters

variable "task_memory" {
  description = "The amount (in MiB) of memory used by the task."
  default     = 2048
}
variable "task_cpu" {
  description = "The number of cpu units used by the task."
  default     = 1024
}

variable "scaling_up_cooldown" {
  description = "The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start (upscaling)."
  default     = "60"
}

variable "scaling_down_cooldown" {
  description = "The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start (downscaling)."
  default     = "300"
}

variable "scaling_up_adjustment" {
  description = " The number of tasks by which to scale, when the upscaling parameters are breached."
  default     = "1"
}

variable "scaling_down_adjustment" {
  description = " The number of tasks by which to scale (negative for downscaling), when the downscaling parameters are breached."
  default     = "-1"
}

variable "task_cpu_low_threshold" {
  description = "The CPU value below which downscaling kicks in."
  default     = "30"
}

variable "task_cpu_high_threshold" {
  description = "The CPU value above which downscaling kicks in."
  default     = "75"
}

variable "max_task" {
  description = "Maximum number of tasks should the service scale to."
  default     = "3"
}

variable "min_task" {
  description = "Minimum number of tasks should the service always maintain."
  default     = "2"
}

# S3 parameters

variable "bucket_name" {
  description = "S3 Bucket for wordpress assets."
  default     = "bucket-wordp-assets"
}
Enter fullscreen mode Exit fullscreen mode

Construção

  • Validação do domínio:

Route 53 (Global)

  • Execute a stack e** 70 recursos** serão criados na conta:

terraform init
terraform plan
terraform apply

  • Aqui já podemos verificar que o site está disponível e basta seguir o padrão de inicialização do wordpress:

  • Para utilizarmos bem os recursos do Amazon CloudFront (CDN) neste cenário wordpress, podemos recorrer aos plugins que permitem utilizar o bucket criado em nossa stack como repositório para os assets (conteúdos estáticos, como imagens):

Wordpress plugin ([https://deliciousbrains.com/wp-offload-media/](https://deliciousbrains.com/wp-offload-media/))Wordpress plugin (https://deliciousbrains.com/wp-offload-media/)

Wordpress plugin ([https://deliciousbrains.com/wp-offload-media/](https://deliciousbrains.com/wp-offload-media/))Wordpress plugin (https://deliciousbrains.com/wp-offload-media/)

Concluindo o passo 4 da construção, o objetivo principal já foi alcançado. Vamos agora entender alguns dos recursos provisionados na stack e seu comportamento.

RDS Aurora Mysql Serverless

RDS

As credenciais são armazenadas no AWS Systems Manager Parameter Store através desse bloco terraform:

resource "aws_ssm_parameter" "db_master_user" {
  name  = "/${var.env_prefix}/${var.environment}/db_master_user"
  type  = "SecureString"
  value = var.db_master_username
  tags  = var.tags
}

resource "aws_ssm_parameter" "db_master_password" {
  name  = "/${var.env_prefix}/${var.environment}/db_master_password"
  type  = "SecureString"
  value = var.db_master_password
  tags  = var.tags
}
Enter fullscreen mode Exit fullscreen mode

ECS (Fargate)

Provisonamos um serviço com duas tasks:

service

tasks

Olhando nos detalhes das tasks onde estamos executando os containers wordpress, podemos validar as variáveis de ambiente de carregamos através da task definiton criada no terraform, também o volume EFS que montamos:

container details

container details

resource "aws_ecs_task_definition" "this" {
  family                   = "${var.env_prefix}-${var.environment}"
  execution_role_arn       = aws_iam_role.task_execution_role.arn
  task_role_arn            = aws_iam_role.task_role.arn
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.task_cpu
  memory                   = var.task_memory
  container_definitions    = <<CONTAINER_DEFINITION
[
  {
    "secrets": [
      {
        "name": "WORDPRESS_DB_USER", 
        "valueFROM": "${aws_ssm_parameter.db_master_user.arn}"
      },
      {
        "name": "WORDPRESS_DB_PASSWORD", 
        "valueFROM": "${aws_ssm_parameter.db_master_password.arn}"
      }
    ],
    "environment": [
      {
        "name": "WORDPRESS_DB_HOST",
        "value": "${aws_rds_cluster.this.endpoint}"
      },
      {
        "name": "WORDPRESS_DB_NAME",
        "value": "${var.db_name}"
      }
    ],
    "essential": true,
    "image": "wordpress",        
    "name": "wordpress",
    "portMappings": [
      {
        "containerPort": 80
      }
    ],
    "mountPoints": [
      {
        "containerPath": "/var/www/html",
        "sourceVolume": "efs"
      }
    ],
    "logConfiguration": {
      "logDriver":"awslogs",
      "options": {
        "awslogs-group": "${aws_cloudwatch_log_group.wordpress.name}",
        "awslogs-region": "${var.aws_region}",
        "awslogs-stream-prefix": "app"
      }
    }
  }
]
CONTAINER_DEFINITION

  volume {
    name = "efs"
    efs_volume_configuration {
      file_system_id = aws_efs_file_system.this.id
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

CloudFront

Na distribuição do cloudfront, criamos duas origins. Uma delas é o ALB público e a outra o bucket S3 para os *assets *do wordpress:

origins

Em behaviors, foi determinado que a ação padrão é encaminhar a requisição diretamente para o ALB que por sua vez direciona para as tasks ECS. Caso a requisição tenha como path pattern o repositório padrão dos assets wordpress, será direcionada para o S3 onde este conteúdo estático está sendo distribuído globalmente através do cloudfront:

behaviors

# Cache behavior with precedence 1
Enter fullscreen mode Exit fullscreen mode
default_cache_behavior {
  allowed_methods  = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
  cached_methods   = ["GET", "HEAD", "OPTIONS"]
  target_origin_id = "alb"

  forwarded_values {
    query_string = true
    headers      = ["*"]

    cookies {
      forward = "all"
    }

  }

  viewer_protocol_policy = "redirect-to-https"
  min_ttl                = 0
  default_ttl            = 0
  max_ttl                = 0
  compress               = true
}

# Cache behavior with precedence 0

ordered_cache_behavior {
  path_pattern     = "/wp-content/uploads"
  allowed_methods  = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
  cached_methods   = ["GET", "HEAD", "OPTIONS"]
  target_origin_id = "s3_assets"

  forwarded_values {
    query_string = true
    headers      = ["Host"]

    cookies {
      forward = "all"
    }
  }

  min_ttl                = 900
  default_ttl            = 900
  max_ttl                = 900
  compress               = true
  viewer_protocol_policy = "redirect-to-https"
}
Enter fullscreen mode Exit fullscreen mode

S3

A estrutura de diretórios do bucket é criada através do plugin utilizado anteriormente. Vale lembrar que, também é anexada uma policy no bucket permitindo acesso do *cloudfront *para distribuição:

s3 bucket

data "aws_iam_policy_document" "s3_assets" {
  statement {
    actions   = ["s3:GetObject"]
    resources = ["${module.s3_bucket.s3_bucket_arn}/*"]

    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values   = [aws_cloudfront_distribution.this.arn]
    }
  }
}

resource "aws_s3_bucket_policy" "s3_assets" {
  bucket = module.s3_bucket.s3_bucket_id
  policy = data.aws_iam_policy_document.s3_assets.json
}
Enter fullscreen mode Exit fullscreen mode

Route 53

Os registros abaixo são inseridos na zona DNS existente, fazendo referência ao ALB, CloudFront e certificado ACM:

public hosted zone

Happy building!

💖 💪 🙅 🚩
paulofponciano
Paulo Ponciano

Posted on January 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related