CDN de alta disponibilidade na AWS com Terraform: um guia para reduzir a latência e aumentar a performance de seu site
Tags: terraform, aws, iac, cloudfront
Published: 2023-03-26 01:35:32

Este artigo é uma continuação do [primeiro sobre o mesmo assunto que foi escrito há algum tempo](https://dev.to/aws-builders/cdn-de-alta-disponibilidade-na-aws-com-s3-cloudfront-e-route53-3bh9), mas sem abordar a utilização de Infrastructure as Code (IaC). Neste artigo, será apresentado um guia de como criar sua própria CDN de alta disponibilidade na AWS usando IaC com Terraform.

Utilize a CDN para hospedar um website estático e/ou servir arquivos estáticos com baixa latência e alta disponibilidade. Além de melhorar o desempenho do website, as CDNs garantem que o usuário sempre será servido do servidor mais próximo, o que reduz ainda mais a latência e garante uma experiência de navegação rápida e suave. Isso pode levar a um aumento de tráfego e conversões.

> ℹ️ Para explicação dos serviços envolvidos, precificação e outras informações, leia o [primeiro artigo sobre o mesmo assunto](https://medium.com/@tiagoboeing/cdn-de-alta-disponibilidade-na-aws-com-s3-cloudfront-e-route53-d08da8fab0ab) que demonstra a configuração via Console da AWS.
> 

É necessário conhecimento do funcionamento do Terraform e da AWS. Não serão abordados aqui conceitos introdutórios sobre essas tecnologias.

## Objetivos

Criar automaticamente toda a infraestrutura necessária dentro da AWS em poucos minutos e com alguns comandos, incluindo:

- Configuração dos serviços necessários;
- Criação dos registros DNS para validar o certificado e um alias para o CloudFront;
- Geração do certificado SSL para servir conteúdo utilizando protocolo HTTPS;

## Pré-requisitos

- Conta AWS;
- AWS CLI devidamente configurado;
- Terraform devidamente instalado e configurado;

## Composição da infraestrutura

- S3 como fonte dos arquivos;
- CloudFront para distribuir os arquivos;
- Route53 para criar os registros DNS no domínio próprio;
- Certificate Manager (ACM) para gerenciar os certificados SSL/TLS para a distribuição do CloudFront.

![Mermaid diagram with IaC structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8d4h651xhmspi11exe1.png)

### IaC - Infrastructure as Code

É uma prática de gerenciamento de infraestrutura de TI em que a infraestrutura é gerenciada por meio de código, em vez de configurações manuais. Com IaC, a infraestrutura é definida em arquivos de configuração que podem ser versionados, testados e implementados automaticamente, permitindo que os desenvolvedores gerenciem a infraestrutura de maneira mais eficiente e consistente. Ferramentas populares de IaC incluem o Terraform, o AWS CloudFormation e o Ansible.

### Terraform

O Terraform é uma ferramenta de IaC que permite definir e provisionar infraestrutura em nuvem de forma declarativa, utilizando arquivos de configuração para descrever os recursos necessários. Com o Terraform, é possível criar, modificar e excluir recursos em diversos provedores de nuvem, como AWS, Azure e Google Cloud Platform, de forma consistente e replicável. Além disso, o Terraform possui recursos avançados, como a possibilidade de criar módulos reutilizáveis e gerenciar o estado da infraestrutura, permitindo uma gestão mais eficiente e segura da infraestrutura em nuvem.

Para mais informações, consulte a [documentação oficial do Terraform](https://www.terraform.io/docs/index.html).

---

> ℹ️ Se desejar apenas o código-fonte para provisionar a infraestrutura [acesse o repositório no GitHub.](https://github.com/tiagoboeing/terraform-aws/tree/master/cloudfront-cdn)
> 

### Código-fonte

- [Link permanente com snapshot do repositório no momento que o artigo foi escrito.](https://github.com/tiagoboeing/terraform-aws/tree/536e087e6604da759f5b56f9ca82e09a773231d5/cloudfront-cdn)

**O projeto Terraform será dividido em vários arquivos para facilitar compreensão:**

- `provider.tf`: Especifica com qual cloud e versões estamos trabalhando;
- `main.tf`: Arquivo principal que centraliza configurações comuns do Terraform, `data`, `locals`, etc;
- `outputs.tf`: Define as saídas dos recursos criados pelo Terraform;
- `inputs.tf`: Define as variáveis de entrada para o Terraform;
- `s3.tf`: Configurações do bucket, políticas e outros;
- `certificate.tf`: Configurações do certificado SSL para servir conteúdo utilizando HTTPS;
- `cloudfront.tf`: Criação e configuração do CloudFront;
- `test-cdn.tf`: Apenas para demonstração e teste do funcionamento, este arquivo fará upload de um arquivo HTML para o bucket e invalidará o cache do CloudFront para que as alterações sejam refletidas rapidamente. Remova após validar a implementação;
- `terraform.tfvars`: Arquivo onde o valor das variáveis pode ser especificado.

> Para visualizar o conteúdo dos arquivos, [confira o repositório no momento que o artigo foi escrito.](https://www.notion.so/CDN-de-alta-disponibilidade-na-AWS-utilizando-IaC-bf8b7053bca349e3bfe4291fa39b37d9) As seções abaixo abordarão apenas arquivos chave para facilitar a compreensão.

## Definição do provedor (`provider.tf`)

```bash
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.59.0"
    }
  }
}

provider "aws" {
  skip_credentials_validation = true
}
```

## Variáveis locais e queries (`main.tf`)

```bash
locals {
  service              = var.service
  stage                = var.stage
  resource_prefix_name = "${var.service}-${var.stage}"
  route53_private_zone = var.route53_private_zone
  route53_base_domain  = var.route53_zone_domain
  cdn_domain           = var.cdn_domain
}

# Get AWS Account ID
data "aws_caller_identity" "current" {}

# Get Route53 Zone
data "aws_route53_zone" "domain_zone" {
  name         = "${local.route53_base_domain}."
  private_zone = local.route53_private_zone
}
```

- São definidas algumas variáveis locais para utilizar nos demais arquivos;
- [aws_caller_identity](https://www.terraform.io/docs/providers/aws/d/caller_identity.html): consulta usada para obter o ID da conta e concatená-lo com o nome do bucket do S3 para garantir que o nome seja único na AWS e tentar evitar falhas na criação do bucket.
- [aws_route53_zone](https://www.terraform.io/docs/providers/aws/d/route53_zone.html): consulta uma Hosted Zone no Route53 que possua o domínio informado.

## Definição das `inputs` (`inputs.tf`)

Na infraestrutura teremos as seguintes `inputs`:

- `service`: nome com que os recursos serão criados dentro da AWS;
- `stage`: estágio da aplicação caso desejar criar mais de uma CDN para o mesmo serviço (dev, test, prod). Nota: é possível utilizar [a implantação contínua do próprio CloudFront](https://docs.aws.amazon.com/pt_br/AmazonCloudFront/latest/DeveloperGuide/continuous-deployment.html);
- `route53_private_zone`: define se a zona DNS é privada ou não;
- `route53_zone_domain`: domínio da zona DNS
- `cdn_domain`: nome do domínio para a CDN, pode ser deixado em branco para utilizar o domínio base (Hosted Zone do Route53);
- `cloudfront_allowed_methods`: métodos HTTP permitidos no CloudFront;
- `cloudfront_cached_methods`: métodos HTTP que serão mantidos em cache no CloudFront;
- `cloudfront_default_root_object`: arquivo raiz padrão no CloudFront;
- `cloudfront_http_version`: versão do protocolo HTTP no CloudFront.

```bash
variable "service" {
  default     = "terraform-cdn"
  type        = string
  description = "Service name"
}

variable "stage" {
  default     = "dev"
  type        = string
  description = "Stage (dev, test, prod)"
}

variable "route53_private_zone" {
  default = false
  type    = bool
}

variable "route53_zone_domain" {
  default     = ""
  type        = string
  description = "Route53 zone domain (base domain)"
}

variable "cdn_domain" {
  default     = ""
  type        = string
  description = "Domain name (Where you want to deploy the CloudFront distribution. Leave empty to deploy inside base domain)"
}

variable "cloudfront_allowed_methods" {
  default = ["GET", "HEAD", "OPTIONS"]
  type    = list(string)
}

variable "cloudfront_cached_methods" {
  default = ["GET", "HEAD", "OPTIONS"]
  type    = list(string)
}

variable "cloudfront_default_root_object" {
  default = "index.html"
  type    = string
}

variable "cloudfront_http_version" {
  default = "http2"
  type    = string
}
```

### Passando valores para as variáveis criadas (`terraform.tfvars`)

Existem várias formas de passar valores para as variáveis do Terraform, incluindo a utilização de arquivos JSON, variáveis de ambiente ou a passagem de valores diretamente pela linha de comando no momento da execução do Terraform. Consulte a [documentação oficial do Terraform](https://www.terraform.io/docs/language/values/variables.html) para mais informações.

Neste exemplo será utilizado um arquivo com a extensão `.tfvars`, então basta criar um arquivo `terraform.tfvars` e definir o seguinte conteúdo:

```bash
route53_zone_domain = "tiagoboeing.com"
cdn_domain          = "terraform-cdn.tiagoboeing.com"
```

Neste caso o domínio (Hosted Zone no Route53) é `tiagoboeing.com` e a CDN será implantada em um subdomínio.

A propriedade `route53_zone_domain` serve como base para o Terraform obter a Hosted Zone no Route53.

## Criação do bucket no S3 (`s3.tf`)

O primeiro passo é criar um bucket no S3 e definir uma política de segurança que restrinja o acesso público direto a qualquer conteúdo. Isso impedirá que um objeto seja tornado público diretamente via S3. O CloudFront será a única maneira de acessar os arquivos.

Crie um arquivo chamado `s3.tf` e adicione o seguinte conteúdo:

```bash
resource "aws_s3_bucket" "bucket" {
  bucket = "${local.resource_prefix_name}-bucket-${data.aws_caller_identity.current.account_id}"

  tags = {
    Stage   = local.stage,
    Service = local.service
  }
}

resource "aws_s3_bucket_acl" "bucket_acl" {
  bucket = aws_s3_bucket.bucket.id
  acl    = "private"
}

resource "aws_s3_bucket_public_access_block" "bucket_public_access" {
  bucket = aws_s3_bucket.bucket.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_policy" "bucket_policy" {
  bucket = aws_s3_bucket.bucket.id

  policy = jsonencode({
    "Version" : "2008-10-17",
    "Statement" : [
      {
        "Sid" : "AllowCloudFrontServicePrincipalReadOnly",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "cloudfront.amazonaws.com",
        },
        "Action" : "s3:GetObject",
        "Resource" : "arn:aws:s3:::${aws_s3_bucket.bucket.bucket}/*",
        "Condition" : {
          "StringEquals" : {
            "aws:SourceArn" : aws_cloudfront_distribution.cloudfront.arn
          }
        }
      }
    ]
  })

  depends_on = [
    aws_s3_bucket.bucket
  ]
}
```

**Estamos definindo os seguintes recursos:**

- [aws_s3_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket): Cria o bucket que o CloudFront utilizará como `origin`.
- [aws_s3_bucket_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy): Define uma política de acesso para que o CloudFront seja capaz de obter os objetos do bucket no S3 (necessário quando trabalhamos com buckets privados)
- [aws_s3_bucket_acl](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_acl): Define o controle de acesso ao bucket no S3 como privado.
- [aws_s3_bucket_public_access_block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block): Define os parâmetros de acesso público para o bucket no S3 da AWS, bloqueando a criação de buckets públicos, políticas públicas, listagens públicas e controle de acesso público.

## Criação e validação do certificado SSL (`certificate.tf`)

```bash
resource "aws_acm_certificate" "certificate" {
  domain_name       = local.cdn_domain != "" ? local.cdn_domain : data.aws_route53_zone.domain_zone.name
  validation_method = "DNS"

  tags = {
    Stage   = local.stage,
    Service = local.service
  }
}

resource "aws_acm_certificate_validation" "certificate_validation" {
  certificate_arn         = aws_acm_certificate.certificate.arn
  validation_record_fqdns = [for record in aws_route53_record.certificate_records : record.fqdn]

  depends_on = [
    aws_acm_certificate.certificate
  ]

  timeouts {
    create = "10m"
  }
}

# Add Certificate Validation Records on Route53
resource "aws_route53_record" "certificate_records" {
  for_each = {
    for dvo in aws_acm_certificate.certificate.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.domain_zone.zone_id
}
```

**Estamos definindo os seguintes recursos:**

- [aws_acm_certificate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate): criação do certificado SSL para a CDN utilizando o AWS Certificate Manager (ACM);
- [aws_acm_certificate_validation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validation): Define que a validação do certificado será realizada via DNS;
- [aws_route53_record](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record): adiciona registros no Route53 para validar o certificado SSL.

## Criação do CloudFront (`cloudfront.tf`)

```bash
resource "aws_cloudfront_origin_access_control" "cloudfront_acl" {
  name = "ACL - ${local.resource_prefix_name}"

  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "cloudfront" {
  enabled             = true
  is_ipv6_enabled     = true
  comment             = "CDN for ${local.resource_prefix_name}"
  default_root_object = var.cloudfront_default_root_object
  http_version        = var.cloudfront_http_version

  aliases = local.cdn_domain != "" ? [local.cdn_domain] : local.route53_base_domain != "" ? [local.route53_base_domain] : []

  origin {
    origin_id                = aws_s3_bucket.bucket.id
    origin_access_control_id = aws_cloudfront_origin_access_control.cloudfront_acl.id
    domain_name              = aws_s3_bucket.bucket.bucket_regional_domain_name
  }

  default_cache_behavior {
    target_origin_id = aws_s3_bucket.bucket.id

    compress        = true
    allowed_methods = var.cloudfront_allowed_methods
    cached_methods  = var.cloudfront_cached_methods

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = false

    minimum_protocol_version = "TLSv1.2_2021"
    ssl_support_method       = "sni-only"

    acm_certificate_arn = aws_acm_certificate_validation.certificate_validation.certificate_arn
  }

  tags = {
    Stage   = local.stage,
    Service = local.service
  }

  depends_on = [
    aws_s3_bucket.bucket
  ]
}

# Create Route53 Record to CloudFront
resource "aws_route53_record" "domain_record" {
  name    = local.cdn_domain != "" ? local.cdn_domain : data.aws_route53_zone.domain_zone.name
  type    = "A"
  zone_id = data.aws_route53_zone.domain_zone.zone_id

  alias {
    name                   = aws_cloudfront_distribution.cloudfront.domain_name
    zone_id                = aws_cloudfront_distribution.cloudfront.hosted_zone_id
    evaluate_target_health = false
  }

  depends_on = [
    aws_cloudfront_distribution.cloudfront
  ]
}
```

**Estamos definindo os seguintes recursos:**

- [aws_cloudfront_origin_access_control](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_identity): Cria uma política de acesso para o S3;
- [aws_cloudfront_distribution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution): Cria e configura a distribuição do CloudFront;
- [aws_route53_record](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record): Adiciona um registro DNS no Route53 como alias para o CloudFront.

## Teste da infraestrutura (`test-cdn.tf`)

Aqui será a etapa onde um arquivo HTML será enviado para o bucket S3 e então o cache do CloudFront invalidado para garantir que as alterações reflitam imediatamente. 

```bash
#############################
# Only for tests purposes
# Upload one HTML file to S3
#############################
resource "aws_s3_object" "public_folder" {
  for_each     = fileset("public/", "*")
  bucket       = aws_s3_bucket.bucket.id
  key          = each.value
  source       = "public/${each.value}"
  etag         = filemd5("public/${each.value}")
  content_type = "text/html"
}

#############################
# Only for tests purposes
# Invalidate index.html from CloudFront cache
#############################
resource "null_resource" "cache_invalidation" {

  # prevent invalidating cache before new s3 file is uploaded
  depends_on = [
    aws_s3_object.public_folder
  ]

  for_each = fileset("${path.module}/public/", "**")

  triggers = {
    hash = filemd5("public/${each.value}")
  }

  provisioner "local-exec" {
    # sleep is necessary to prevent throttling when invalidating many files; a dynamic sleep time would be more reliable
    # possible way of dealing with parallelism (though would lose the indiviual triggers): https://discuss.hashicorp.com/t/specify-parallelism-for-null-resource/20884/2
    command = "sleep 1; aws cloudfront create-invalidation --distribution-id ${aws_cloudfront_distribution.cloudfront.id} --paths '/${each.value}'"
  }
}
```

Crie uma pasta chamada `public` e adicione um arquivo `index.html` com qualquer conteúdo apenas para teste.

## Provisionar infraestrutura

Com os recursos configurados, é hora de provisionar a infraestrutura.

```bash
# Valide o plano de criação
terraform plan

# Se tudo estiver correto, aplique o plano para iniciar a criação
terraform apply
```

O provisionamento inicial normalmente leva alguns minutos e a distribuição do CloudFront pode demorar mais alguns minutos para ficar disponível.

Aguarde o tempo necessário. Ao finalizar, acesse o domínio configurado e valide se o arquivo HTML foi apresentado corretamente.

> No exemplo do artigo o subdomínio onde a CDN foi provisionada é [`terraform-cdn.tiagoboeing.com`](https://terraform-cdn.tiagoboeing.com/)
> 

---

Parabéns! Você agora possui uma CDN de alta disponibilidade na AWS utilizando IaC. Não hesite em explorar outras soluções e recursos da AWS e continue aprendendo!

## Links do artigo

- [Link permanente do repositório no momento que o artigo foi escrito](https://github.com/tiagoboeing/terraform-aws/tree/536e087e6604da759f5b56f9ca82e09a773231d5/cloudfront-cdn)
- [Repositório no GitHub](https://github.com/tiagoboeing/terraform-aws/tree/master/cloudfront-cdn)
- [Demonstração da CDN](https://terraform-cdn.tiagoboeing.com/)

## Leia também

- [CDN de alta disponibilidade na AWS com S3, CloudFront e Route53](https://tiagoboeing.medium.com/cdn-de-alta-disponibilidade-na-aws-com-s3-cloudfront-e-route53-d08da8fab0ab)
- [Execução agendada de funções utilizando AWS e Serverless Framework](https://tiagoboeing.medium.com/execucao-agendada-de-funcoes-em-serverless-2bc7506cfcaf)

## English

- [Serverless Framework + AWS: automatically creating certificate and domain for your app](https://tiagoboeing.medium.com/serverless-framework-aws-automatically-creating-certificate-and-domain-for-your-app-98cd5e31b66c)

## Redes sociais

Quer saber mais sobre mim, o autor deste artigo? Então dê uma olhada nas minhas redes sociais:

- [Perfil no LinkedIn](https://www.linkedin.com/in/tiagoboeing/)
- [Perfil no Medium](https://tiagoboeing.medium.com/)
- [GitHub](https://github.com/tiagoboeing)
- [Instagram](https://www.instagram.com/tiagoboeing/)

Espero que tenha gostado do artigo e encontrado informações úteis. Se tiver alguma dúvida ou sugestão, deixe nos comentários.

---

Você sabia que pode se tornar um Community Builder da AWS? Como Community Builder, você terá a oportunidade de se conectar com outros profissionais da área de tecnologia e aprender com especialistas da AWS. Além disso, você poderá compartilhar seus conhecimentos e experiências com outros membros da comunidade. Não perca a chance de se tornar um líder na sua área e se juntar a uma comunidade global de entusiastas da AWS! Saiba mais em [aws.amazon.com/developer/community/community-builders](https://aws.amazon.com/developer/community/community-builders/).