Ambiente wordpress com CDN provisionado como código — Terraform
Tags: community, cloudfront, aws, terraform
Published: 2024-01-29 21:15:57

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

![](https://cdn-images-1.medium.com/max/2800/1*pCRUp5yFPt_nUENMlM9WqQ.png)

![Arquitetura](https://cdn-images-1.medium.com/max/2680/1*mjus33KjrKYnSPwmXvxMOw.png)

---

[Código terraform aqui.](https://github.com/paulofponciano/ECSFargateCDNWordpressAutoStack.git)

### Pré-requisitos

* Domínio público configurado no Route 53

* $

### Variáveis

```tf
# 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"
}
```

### Construção

- Validação do domínio:

![Route 53 (Global)](https://cdn-images-1.medium.com/max/4084/1*95bH4O6ZAYt-Yp2oMAqw4w.png)

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

`terraform init`
`terraform plan`
`terraform apply`

![](https://cdn-images-1.medium.com/max/4416/1*UK-NVF0VKyZZNh0k8lRlFw.png)

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

![](https://cdn-images-1.medium.com/max/2334/1*lxOCR1uCTcwzqSJXASWVKw.png)

- 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/))](https://cdn-images-1.medium.com/max/2000/1*xlG4CflO8qGHkqf3BDNWPA.png)*Wordpress plugin ([https://deliciousbrains.com/wp-offload-media/](https://deliciousbrains.com/wp-offload-media/))*

![Wordpress plugin ([https://deliciousbrains.com/wp-offload-media/](https://deliciousbrains.com/wp-offload-media/))](https://cdn-images-1.medium.com/max/2000/1*BNltlbedK_RSu6VH2yrNlQ.png)*Wordpress plugin ([https://deliciousbrains.com/wp-offload-media/](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](https://cdn-images-1.medium.com/max/3172/1*4qHuKAQ8KtcGgCEkTIUsMQ.png)

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

```tf
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
}
```
**ECS (Fargate)**

Provisonamos um serviço com duas *tasks*:

![service](https://cdn-images-1.medium.com/max/3092/1*aYr2o3r_A94cq8zTnMi0gg.png)

![tasks](https://cdn-images-1.medium.com/max/3094/1*P2JozkIqsZECfB5D8OJwUw.png)

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](https://cdn-images-1.medium.com/max/2368/1*CIcZnRNGv8ixPe6kfrYfgw.png)

![container details](https://cdn-images-1.medium.com/max/2000/1*JM42u6LcSHgSnrmhV0PW0g.png)

```tf
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
    }
  }
}
```
**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](https://cdn-images-1.medium.com/max/2606/1*6qsFIb2im2Bwwdg_nmb4PQ.png)

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](https://cdn-images-1.medium.com/max/2000/1*k6DwzyG7DbSh1KK_XPKsSQ.png)

    # Cache behavior with precedence 1
    
```tf
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"
}
```

**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](https://cdn-images-1.medium.com/max/3186/1*fvoAxhIZYpEEuJD4Rej1Sw.png)

```tf
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
}
```

**Route 53**

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

![public hosted zone](https://cdn-images-1.medium.com/max/3012/1*hVT9F1ZhueLZZA-1qs-h6w.png)

Happy building!